From 18434878bfcea5179c9fbb88e06dea05e8ba95dc Mon Sep 17 00:00:00 2001
From: Steven van der Vegt <steven.vandervegt@nedap.com>
Date: Fri, 13 Dec 2024 07:44:38 +0000
Subject: [PATCH] Refactor tests and issuer code (#28)

* Change issuer tests to use static test certificate chain
Removes dependency from cert-creator

* Refactor issuer into smaller components
Issuer now accepts checked data. Introduced helper functions which
creates this checked data. Components can be tested separately. It returns a credential object instead of a string so the fields can be inspected during tests.
---
 .gitignore                             |   2 +-
 go.mod                                 |   1 -
 go.sum                                 |   6 -
 main.go                                |  26 +-
 uzi_vc_issuer/testdata/signing_key.pem |  27 ++
 uzi_vc_issuer/testdata/valid_chain.pem |  80 +++++
 uzi_vc_issuer/ura_issuer.go            | 240 ++++++++++---
 uzi_vc_issuer/ura_issuer_test.go       | 446 ++++++++++++-------------
 8 files changed, 529 insertions(+), 299 deletions(-)
 create mode 100644 uzi_vc_issuer/testdata/signing_key.pem
 create mode 100644 uzi_vc_issuer/testdata/valid_chain.pem

diff --git a/.gitignore b/.gitignore
index d3a8764..16a4a6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-*.pem
+/*.pem
 !ca.pem
 uzi-did-x509-issuer
 c.out
diff --git a/go.mod b/go.mod
index 1aab9ab..9f9906a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.23.1
 require (
 	github.com/alecthomas/kong v1.4.0
 	github.com/google/uuid v1.6.0
-	github.com/huandu/go-clone v1.7.2
 	github.com/lestrrat-go/jwx/v2 v2.1.2
 	github.com/nuts-foundation/go-did v0.15.0
 	github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index d61d4f5..47cc5cb 100644
--- a/go.sum
+++ b/go.sum
@@ -15,10 +15,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
-github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
-github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo=
-github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
 github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
 github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@@ -48,7 +44,6 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr
 github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs=
 github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
@@ -59,7 +54,6 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
index 71ac94a..069cbb4 100644
--- a/main.go
+++ b/main.go
@@ -50,6 +50,7 @@ func main() {
 			fmt.Println(err)
 			os.Exit(-1)
 		}
+		fmt.Println("VC result:")
 		err = printLineAndFlush(jwt)
 		if err != nil {
 			fmt.Println(err)
@@ -123,5 +124,28 @@ func printLineAndFlush(jwt string) error {
 }
 
 func issueVc(vc VC) (string, error) {
-	return uzi_vc_issuer.Issue(vc.CertificateFile, vc.SigningKey, vc.SubjectDID, vc.Test, vc.IncludePermanent, vc.SubjectAttributes)
+	chain, err := uzi_vc_issuer.NewValidCertificateChain(vc.CertificateFile)
+	if err != nil {
+		return "", err
+	}
+
+	key, err := uzi_vc_issuer.NewPrivateKey(vc.SigningKey)
+	if err != nil {
+		return "", err
+	}
+
+	subject, err := uzi_vc_issuer.NewSubjectDID(vc.SubjectDID)
+	if err != nil {
+		return "", err
+	}
+
+	credential, err := uzi_vc_issuer.Issue(chain, key, subject,
+		uzi_vc_issuer.SubjectAttributes(vc.SubjectAttributes...),
+		uzi_vc_issuer.AllowTestUraCa(vc.Test))
+
+	if err != nil {
+		return "", err
+	}
+
+	return credential.Raw(), nil
 }
diff --git a/uzi_vc_issuer/testdata/signing_key.pem b/uzi_vc_issuer/testdata/signing_key.pem
new file mode 100644
index 0000000..8ce4f05
--- /dev/null
+++ b/uzi_vc_issuer/testdata/signing_key.pem
@@ -0,0 +1,27 @@
+-----BEGIN PRIVATE KEY-----
+MIIEpAIBAAKCAQEA0+LyiNeyaWUCHyzRDKG6+a+btQXCiuKX7cRzLWRp3Dc0v4Bv
+8GCgNNSHRwHz+KbadpGuBebvD6f4GvwDA9PjdJ3xjjRi0aF1hYmzPmnrsYxDtuZV
+A1IU0BTBbfQcQ9yXpUEtPZwDvf3/J9OlckNrEVOKZzG7Bw/0rk4OGt4D4lOkmhlg
+yvWHou59J9/ARrRaRl1b5bcjzrUnEbD6B8w3G6xino57/M2cvExFu3dChV6eABDD
+hwE+WsSK2CaijXMiCD25xg80nq9gzTIJEVlSEYilf4DthdyaRae4WV74/x5H3EZI
+wRHr+8A2rZmNXivpUuDXAtTH5BsJibujfeYfJQIDAQABAoIBAQCouZC+bVyR1rBA
+2PRC5cq5JyCLnuGSrOukl4nL/Kjbhk6HrCP3O0p3p0Ftxt1bBKr0Pf9gjcuSIQRN
+oJ5Z/vGiHF+NCKQkIDkwND26lqfrwzDsxS+vLD6Mj+qTvw5+73sGSgdXhxPnyAnV
+0hBuE8d/jZGpqQ0wi4EhB+DtfhuDrfp4ZdQXFynsTi/MuttOmiu4r/aVqYS4uhjh
+NO4EvnfDpyUVxpdy78t8H16+ghyAI5fnELqxq7Gk1EIrFYtUfnEfJKyB33ZLZrkL
+imIVc5Qt90zObFFljDrM26cr7OnKopApO98dREi13aLio2QeKX3OkUOIlcGgwAN0
+Fhpvv7jBAoGBAOodh925HBKiIJlE3C5oYwnalVLnr7fVffACuCyxE2oxPv7jl1qP
+XnJ1i5DIgwyqUBzLbgo6Jf9fVTK/0lzx0rMIDBTiMJE+YE7UZ1/BFsdql0AnkHP9
+S21E3vdVPl1vcQMbc9fokkGYayMDkLeToLKVIv/76IZkQ1iS6vpWwQqVAoGBAOex
+eNmXms9FSx2aw3IdAHnS11u+cvn60hYmvVod10kFzhO2l17WBtlWSzysWchEWzX3
+u0bW95LIuRerlj463jqw89ecEadmOsn382Icpsf2ZhtswyeAIa7u0/y965xX6j0+
+dbx1GQ8Ftdt9nozb/o41sF5/DjrYWmQFmOrvfy5RAoGAY0/9p7/zua/O9lWwtXsQ
+sEhqWc3wy6IkF2F/8W14l+6mE4hGV2NEJHfaqaN1fDTvYRem6W27WrZ9NNcMjOME
+h2/deCpvgd2dCzOtWoBVgmikGtHtxFZp3cN+dhtSJl606SWHIcsF6A+ZOzQy+r0E
+SV1ciIy7Ge+EZhmE1odgwnUCgYBatXa06c/oOh7Qdljygjw/dbZu6r8k83fwyDX1
+5Bz3L9igiyn0LSL9T/WgyXFVIL39AQJHF75Rr1gX1ku6DV4X6FNvJGEdAr8dd3/H
+96OsQeFz9z7oZhfJ3yMLnmdyDFFerOd3YvjukrPCPQon57Ffh9GHDYNKso2g/zgB
+Msa+IQKBgQDEpBtnP0+oJCAlUb3bOqJY7+5B4GsBCbNgV2rpa362Iyx5OPKLdGgV
+U/G3jdZeqrzLJsFPfU41jOFiL76pDsH0rCUK1fQJkAhrux/k/5rYWhgfziZYwiNT
+RccjKo7mMgg/vPjbw/wtLhvLTstfMQl5OLJ0f/vROR24ThAdNiFGWw==
+-----END PRIVATE KEY-----
diff --git a/uzi_vc_issuer/testdata/valid_chain.pem b/uzi_vc_issuer/testdata/valid_chain.pem
new file mode 100644
index 0000000..db2f6fa
--- /dev/null
+++ b/uzi_vc_issuer/testdata/valid_chain.pem
@@ -0,0 +1,80 @@
+-----BEGIN CERTIFICATE-----
+MIIDjTCCAnWgAwIBAgICAMQwDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UEChMXSW50
+ZXJtZWRpYXRlIENBIExldmVsIDIwHhcNMjQxMjAzMTQ1NTIyWhcNMjUwMTAyMTQ1
+NTIyWjAlMREwDwYDVQQKEwhGYXV4Q2FyZTEQMA4GA1UEBRMHMTExMTExMTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPi8ojXsmllAh8s0Qyhuvmvm7UF
+woril+3Ecy1kadw3NL+Ab/BgoDTUh0cB8/im2naRrgXm7w+n+Br8AwPT43Sd8Y40
+YtGhdYWJsz5p67GMQ7bmVQNSFNAUwW30HEPcl6VBLT2cA739/yfTpXJDaxFTimcx
+uwcP9K5ODhreA+JTpJoZYMr1h6LufSffwEa0WkZdW+W3I861JxGw+gfMNxusYp6O
+e/zNnLxMRbt3QoVengAQw4cBPlrEitgmoo1zIgg9ucYPNJ6vYM0yCRFZUhGIpX+A
+7YXcmkWnuFle+P8eR9xGSMER6/vANq2ZjV4r6VLg1wLUx+QbCYm7o33mHyUCAwEA
+AaOByTCBxjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYD
+VR0TAQH/BAIwADAfBgNVHSMEGDAWgBQkmFoX/Ybgyfv2zOJ1cYtzyHWMWTBwBgNV
+HREEaTBnoEIGA1UFBaA7FjkyLjE2LjUyOC4xLjEwMDcuOTkuMjExMC0xLTExMTEx
+MTEtUy0yMjIyMjIyLTAwLjAwMC0zMzMzMzOgIQYIKwYBBQUHCAOgFTATEwcyMjIy
+MjIyBghghBABh28DAzANBgkqhkiG9w0BAQsFAAOCAQEActbKEDR5er6Yj21HHVSl
+XSanPuZsL3z516khvWJIihKWQ3ByPSVxBajQxY4YQAWQKw++fhkCwBrGTxo+7fHa
+warPLUXOr2Wx2QidKtxjndWnv2o9Wn0QCI3jIIJYfgnyfqXKWR5AFseEJIaPSuEK
+q02KhnYVL+gzyW4RVAvZoHSaFSALzS8D7uk51C0Z1hp4clueHkRSYy8GzShe/dJ5
+e3vVqsMCjV3/YOp/Z8bLGHk1xFViCX8jDYbM6+DWKWCk1IWJeDZoPwBN6cm6tkga
+Puhm/aAGxKWgio76j6Fw2h5u0W+oUp8FKzMSa/4VrWoFCO98uPowg0OMl6Cnqiji
+YQ==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDRDCCAiygAwIBAgICAMcwDQYJKoZIhvcNAQELBQAwIjEgMB4GA1UEChMXSW50
+ZXJtZWRpYXRlIENBIExldmVsIDEwHhcNMjQxMjAzMTQ1NTIyWhcNMjUwMTAyMTQ1
+NTIyWjAiMSAwHgYDVQQKExdJbnRlcm1lZGlhdGUgQ0EgTGV2ZWwgMjCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbwiYBkjgNHyyullehHoEf1BlKXjqKC
+KU49x1kcoqHk7K5jD4M1+f0Lzk6zgj509KIWFG2XxCkC7/omRxaDuwt2xjQQJC5J
+wxePma8yNbhHsEGjKyGHBParpJaiYjfPBGJwwKvxl7z4xLc8hMO7BW1fJFbU78zo
+1ZzXhPkOvYJQLKkFY7HR6EtvFTNztbO9fxY7eSjK49ot4WVIAo00iQ+N/uLiQPmW
+Ph/vpMxkvyVv95sbCYcX0r7jE0Hxm5+nDnV3G32Me6RTo7qR12zhEgP9+kneM9qT
+CajVHIkXxMIcNRn09LmsISq8P/7sDqQ5dV8fp9b1LZbxbLTpUi1/4p0CAwEAAaOB
+gzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
+BwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFCSYWhf9huDJ+/bM4nVxi3PI
+dYxZMB8GA1UdIwQYMBaAFDI0sgMhu04pBJ7LaXEUAE1a4F3sMA0GCSqGSIb3DQEB
+CwUAA4IBAQAsYAwb1iyntORynMjUwtS37dTV8u8oQgLdCfPhf29I0HkT/QgJae/X
+XYTxTCHY8Ks8dyWqA6so0bscZLmGkOyoashufwhoiEuYTQJsAeneObNPPqdSjiM7
+aoqFHbtyQoAUd0sUFrCQ45Pgc9NsXf/0Nlo2WhOeq8JfjwjE5Ya2VP/5LSTM8jl7
+duZT8pqtQzClMTtGhs4Ie6ULMMwhy6p5tdRvIbzp3nYuexW4RDiW+sGZuJUeGDpz
+90eL9irIOQRfeM9fFg5EvKdx2e38QsdrUAwOPFWGCsTar9JNi4J/rrhNRMxLSTKt
+8UBYFp7OwV0DOa2hF4dpmZwIRtiSuuiC
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDEDCCAfigAwIBAgIBLTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQKEwdSb290
+IENBMB4XDTI0MTIwMzE0NTUyMloXDTI1MDEwMjE0NTUyMlowIjEgMB4GA1UEChMX
+SW50ZXJtZWRpYXRlIENBIExldmVsIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDBGDihUQ0osFYifpR3rBy39+OpDYJbSVqQ1jeNN7iF50HkqBzQ+QMS
+KgXnp9OF+o7yO4U+UDxwrEJaJfUtra+yQi8nltRmO/yimA3t4m/eXnGOB4puMyUH
+rRlKwClkH38wIUK07Zi4bT0EP3IJTNDAZysOQA7Y4pzbUpRN5gZtSwWu0Vf1B/bJ
+/aTKWOVJO0xA48UsmA3T5UM+jDBMwxPhwdrzP0CjngVhpnQtxEX8wPfjonEqeQ7q
+ipAvgFkElT+eUSE5Osz4i994OTVSNgpiGDHS13CVy/QPdNXGiQX1cdAip0fCe5Zh
+NEhT2v09U+DNZ7F3ChfeuD2PIi2jLYSrAgMBAAGjYTBfMA4GA1UdDwEB/wQEAwIC
+hDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB
+/zAdBgNVHQ4EFgQUMjSyAyG7TikEnstpcRQATVrgXewwDQYJKoZIhvcNAQELBQAD
+ggEBANHq7JwEc8ohGLBTNYg3dkIcEw2T/yl0Dbs+XI6QtXOFswUDHLgOjYHeJulc
++BvnDx2N58X49Eqqq3PrSAdchECd/YDRU9RAZGzFAeZanoMkDwRv90F/hsblKRGO
+A5T7d310dU6L4NjNcZvI90ICahP6YLrD4ycR2OLCDi9pgqoFfAJF7rbMx39QhxqH
+PB9BHtbh2+G9ZgL3S4h2PUTipcTJfz3LisdP36kBDqKS4BaAi0kes3/iffEU3Q+X
+ZsHTnOoDztDi3I5OJipCkiwW8RJsojgZJBjSi+S9fuzg0MoapkXqActBXNGvAv7u
+0jq8yWL9HNVqFANOocePdku5AGA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDATCCAemgAwIBAgICAN0wDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEChMHUm9v
+dCBDQTAeFw0yNDEyMDMxNDU1MjFaFw0yNTAxMDIxNDU1MjFaMBIxEDAOBgNVBAoT
+B1Jvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDj9RegDJEK
+LJ5CCyKtaGH4HeAripSik/eB46FYVFT2x5GzzSAB/0AgRe+yAEjCqDCe+a/AcC8q
+P5a1weguaqfyM+aK/wwXUwK6J/abSNkyFgF6XPY2IJEq9b6pRFI0DPWJ6TniIXAA
+7ehKFxGcBRGoRMgC47S9u8vrZzoQXgohO8A0Z3TdkajXy6BVdGuzWBhFqN9Ueqqi
+4KaUOmNYLjbcvaixC5CfFTn8yHWvZ9JIadnRdzhOpUcGaUjqGemRVdpZ2mShGP+4
+AoeqEoJpRwR7jUoW2oxmep4tCk3Xr/jI2vdsri/dOzo3QX6L+rLRP7LHyUhCYOnt
+pSaLdhEhsjtRAgMBAAGjYTBfMA4GA1UdDwEB/wQEAwIChDAdBgNVHSUEFjAUBggr
+BgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUykNy
+DzIzHIW+cmUmSedYkTmqID4wDQYJKoZIhvcNAQELBQADggEBAKEfcBmTu/oi5qA5
+BQRbk1HSZ23dmg1J2jiB7ObsY+4Gf0tIBknla5x0o4VWOfu2CeeocLF8oV1FuIwK
+msyXrUhZxmmTI+bsL01f1/VMgdXM1OJdjBtmdijmq+NSPyTZw0MMP6kGXjgoI/Ip
+nWfqC3bq3Tkxe2m/9SL3EXC3FHWWzb7c6PsZl5sgF6lurPGTyH6L+syYiKv2/uyc
+NBzSu6ED5rdaBV68oc/QrCPhorbHVgOUq0TBjniqtNAYOTbtK7nsult6Xf5hlFaS
++KkbO3eIgkiINOBDBZg6c9TUYIu+SP9/27j60xoHjumEmayi94smeJAQ5qqkjTY6
+O6Qln1c=
+-----END CERTIFICATE-----
diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go
index c7beaa8..ad02e61 100644
--- a/uzi_vc_issuer/ura_issuer.go
+++ b/uzi_vc_issuer/ura_issuer.go
@@ -8,9 +8,11 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
-	"github.com/nuts-foundation/go-did/did"
+	"os"
 	"time"
 
+	"github.com/nuts-foundation/go-did/did"
+
 	"github.com/google/uuid"
 	"github.com/lestrrat-go/jwx/v2/cert"
 	"github.com/lestrrat-go/jwx/v2/jwa"
@@ -18,82 +20,193 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jwt"
 	ssi "github.com/nuts-foundation/go-did"
 	"github.com/nuts-foundation/go-did/vc"
-	"github.com/nuts-foundation/uzi-did-x509-issuer/ca_certs"
 	"github.com/nuts-foundation/uzi-did-x509-issuer/did_x509"
-	pem2 "github.com/nuts-foundation/uzi-did-x509-issuer/pem"
-	"github.com/nuts-foundation/uzi-did-x509-issuer/uzi_vc_validator"
 	"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
 )
 
-// Issue generates a URA Verifiable Credential using provided certificate, signing key, subject DID, and subject name.
-func Issue(certificateFile string, signingKeyFile string, subjectDID string, allowTestUraCa bool, includePermanentIdentifier bool, subjectAttributes []x509_cert.SubjectTypeName) (string, error) {
-	pemBlocks, err := pem2.ParseFileOrPath(certificateFile, "CERTIFICATE")
+// filename represents a valid file name. The file must exist.
+type fileName string
+
+// nonEmptyBytes represents a non-empty byte slice.
+type nonEmptyBytes []byte
+
+// newFileName creates a new fileName from a string. It returns an error if the file does not exist.
+func newFileName(name string) (fileName, error) {
+	if _, err := os.Stat(name); err != nil {
+		return fileName(""), err
+	}
+
+	return fileName(name), nil
+}
+
+// readFile reads a file and returns its content as nonEmptyBytes. It returns an error if the file does not exist or is empty.
+func readFile(name fileName) (nonEmptyBytes, error) {
+	bytes, err := os.ReadFile(string(name))
 	if err != nil {
-		return "", err
+		return nil, err
+	}
+	if len(bytes) == 0 {
+		return nil, errors.New("file is empty")
 	}
-	allowSelfSignedCa := len(pemBlocks) > 1
-	if len(pemBlocks) == 1 {
-		certificate := pemBlocks[0]
-		pemBlocks, err = ca_certs.GetDERs(allowTestUraCa)
+	return nonEmptyBytes(bytes), nil
+}
+
+// pemBlocks represents a list of one or more PEM blocks.
+type pemBlocks []*pem.Block
+
+// parsePemBytes parses a nonEmptyBytes slice into a pemBlocks
+// it returns an error if the input does not contain any PEM blocks.
+func parsePemBytes(f nonEmptyBytes) (pemBlocks, error) {
+	blocks := make([]*pem.Block, 0)
+	for {
+		block, rest := pem.Decode(f)
+		if block == nil {
+			break
+		}
+		blocks = append(blocks, block)
+		f = rest
+	}
+
+	if len(blocks) == 0 {
+		return nil, errors.New("no PEM blocks found")
+	}
+
+	return blocks, nil
+}
+
+// parseCertificatesFromPemBlocks parses a list of PEM blocks into a list of x509.Certificate instances.
+// It returns an error if any of the blocks cannot be parsed into a certificate.
+func parseCertificatesFromPemBlocks(blocks pemBlocks) (certificateList, error) {
+	certs := make([]*x509.Certificate, 0)
+	for _, block := range blocks {
+		cert, err := x509.ParseCertificate(block.Bytes)
 		if err != nil {
-			return "", err
+			return nil, err
 		}
-		pemBlocks = append(pemBlocks, certificate)
+		certs = append(certs, cert)
 	}
+	return certs, nil
+}
+
+// certificateList represents a non empty slice of x509.Certificate instances.
+type certificateList []*x509.Certificate
+
+// validCertificateChain represents a valid certificate chain.
+type validCertificateChain certificateList
+type privateKey *rsa.PrivateKey
+type subjectDID string
+
+// issueOptions contains values for options for issuing a UZI VC.
+type issueOptions struct {
+	allowTestUraCa             bool
+	includePermanentIdentifier bool
+	subjectAttributes          []x509_cert.SubjectTypeName
+}
+
+// Option is an interface for a function in the options pattern.
+type Option = func(*issueOptions)
+
+// X509Credential represents a JWT encoded X.509 credential.
+type X509Credential string
+
+var defaultIssueOptions = &issueOptions{
+	allowTestUraCa:             false,
+	includePermanentIdentifier: false,
+	subjectAttributes:          []x509_cert.SubjectTypeName{},
+}
+
+func NewValidCertificateChain(fileName string) (validCertificateChain, error) {
+	certFileName, err := newFileName(fileName)
 
-	signingKeys, err := pem2.ParseFileOrPath(signingKeyFile, "PRIVATE KEY")
 	if err != nil {
-		return "", err
+		return nil, err
 	}
-	if len(signingKeys) == 0 {
-		err := fmt.Errorf("no signing keys found")
-		return "", err
+
+	fileBytes, err := readFile(certFileName)
+	if err != nil {
+		return nil, err
 	}
-	privateKey, err := x509_cert.ParsePrivateKey(signingKeys[0])
+	pemBlocks, err := parsePemBytes(fileBytes)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	certs, err := x509_cert.ParseCertificates(pemBlocks)
+	certs, err := parseCertificatesFromPemBlocks(pemBlocks)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
 
-	chain, err := BuildCertificateChain(certs)
+	chain, err := newCertificateChain(certs)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
-	err = validateChain(chain)
+
+	return chain, nil
+}
+
+func NewPrivateKey(fileName string) (privateKey, error) {
+	keyFileName, err := newFileName(fileName)
 	if err != nil {
-		return "", err
+		return nil, err
 	}
-	types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}
-	if includePermanentIdentifier {
-		types = append(types, x509_cert.SanTypePermanentIdentifierValue)
-		types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)
+
+	keyFileBytes, err := readFile(keyFileName)
+	if err != nil {
+		return nil, err
 	}
-	credential, err := BuildUraVerifiableCredential(chain, privateKey, subjectDID, subjectAttributes, types...)
+
+	keyBlocks, err := parsePemBytes(keyFileBytes)
 	if err != nil {
-		return "", err
+		return nil, err
+	}
+
+	key, err := newRSAPrivateKey(keyBlocks)
+	if err != nil {
+		return nil, err
+	}
+
+	return key, nil
+}
+
+func NewSubjectDID(did string) (subjectDID, error) {
+	return subjectDID(did), nil
+}
+
+// newRSAPrivateKey parses a DER-encoded private key into an *rsa.PrivateKey.
+// It returns an error if the key is not in PKCS8 format or not an RSA key.
+func newRSAPrivateKey(pemBlocks pemBlocks) (privateKey, error) {
+	if len(pemBlocks) != 1 || pemBlocks[0].Type != "PRIVATE KEY" {
+		return nil, errors.New("expected exactly one private key block")
 	}
-	jwtString := credential.Raw()
-	validator := uzi_vc_validator.NewUraValidator(allowTestUraCa, allowSelfSignedCa)
-	err = validator.Validate(jwtString)
+	block := pemBlocks[0]
+
+	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
 	if err != nil {
-		return "", err
+		key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if _, ok := key.(*rsa.PrivateKey); !ok {
+		return nil, fmt.Errorf("key is not RSA")
 	}
-	return jwtString, nil
+	return key.(*rsa.PrivateKey), err
 }
 
-// BuildUraVerifiableCredential constructs a verifiable credential with specified certificates, signing key, subject DID.
-func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (*vc.VerifiableCredential, error) {
-	if len(chain) == 0 {
-		return nil, errors.New("empty certificate chain")
+func Issue(chain validCertificateChain, key privateKey, subject subjectDID, optionFns ...Option) (*vc.VerifiableCredential, error) {
+	options := defaultIssueOptions
+	for _, fn := range optionFns {
+		fn(options)
 	}
-	if signingKey == nil {
-		return nil, errors.New("signing key is nil")
+
+	types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}
+	if options.includePermanentIdentifier {
+		types = append(types, x509_cert.SanTypePermanentIdentifierValue)
+		types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)
 	}
-	did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], subjectAttributes, types...)
+
+	did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...)
 	if err != nil {
 		return nil, err
 	}
@@ -107,7 +220,7 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
 	if err != nil {
 		return nil, err
 	}
-	subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...)
+	subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, options.subjectAttributes...)
 	if err != nil {
 		return nil, err
 	}
@@ -119,11 +232,11 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
 	if uzi != serialNumber {
 		return nil, errors.New("serial number does not match UZI number")
 	}
-	template, err := uraCredential(did, signingCert.NotAfter, otherNameValues, subjectTypes, subjectDID)
+	template, err := uraCredential(did, signingCert.NotAfter, otherNameValues, subjectTypes, subject)
 	if err != nil {
 		return nil, err
 	}
-	credential, err := vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) {
+	return vc.CreateJWTVerifiableCredential(context.Background(), *template, func(ctx context.Context, claims map[string]interface{}, headers map[string]interface{}) (string, error) {
 		token, err := convertClaims(claims)
 		if err != nil {
 			return "", err
@@ -157,13 +270,30 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
 			return "", err
 		}
 
-		sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS512, signingKey, jws.WithProtectedHeaders(hdrs)))
+		sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS512, rsa.PrivateKey(*key), jws.WithProtectedHeaders(hdrs)))
 		return string(sign), err
 	})
-	if err != nil {
-		return nil, err
+}
+
+// AllowTestUraCa allows the use of Test URA server certificates.
+func AllowTestUraCa(allow bool) Option {
+	return func(o *issueOptions) {
+		o.allowTestUraCa = allow
+	}
+}
+
+// IncludePermanentIdentifier includes the permanent identifier in the UZI VC.
+func IncludePermanentIdentifier(include bool) Option {
+	return func(o *issueOptions) {
+		o.includePermanentIdentifier = include
+	}
+}
+
+// SubjectAttributes sets the subject attributes to include in the UZI VC.
+func SubjectAttributes(attributes ...x509_cert.SubjectTypeName) Option {
+	return func(o *issueOptions) {
+		o.subjectAttributes = attributes
 	}
-	return credential, nil
 }
 
 // marshalChain converts a slice of x509.Certificate instances to a cert.Chain, encoding each certificate as PEM.
@@ -198,11 +328,11 @@ func validateChain(certs []*x509.Certificate) error {
 	return errors.New("failed to find a path to the root certificate in the chain, are you using a (Test) URA server certificate (Hint: the --test mode is required for Test URA server certificates)")
 }
 
-// BuildCertificateChain constructs a certificate chain from a given list of certificates and a starting signing certificate.
+// newCertificateChain constructs a valid certificate chain from a given list of certificates and a starting signing certificate.
 // It recursively finds parent certificates for non-root CAs and appends them to the chain.
 // It assumes the list might not be in order.
 // The returning chain contains the signing cert at the start and the root cert at the end.
-func BuildCertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) {
+func newCertificateChain(certs certificateList) (validCertificateChain, error) {
 	var signingCert *x509.Certificate
 	for _, c := range certs {
 		if c != nil && !c.IsCA {
@@ -264,7 +394,7 @@ func convertHeaders(headers map[string]interface{}) (jws.Headers, error) {
 
 // uraCredential generates a VerifiableCredential for a given URA and UZI number, including the subject's DID.
 // It sets a 1-year expiration period from the current issuance date.
-func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID string) (*vc.VerifiableCredential, error) {
+func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID subjectDID) (*vc.VerifiableCredential, error) {
 	iat := time.Now()
 	subject := map[string]interface{}{
 		"id": subjectDID,
diff --git a/uzi_vc_issuer/ura_issuer_test.go b/uzi_vc_issuer/ura_issuer_test.go
index 4e627cd..2aba303 100644
--- a/uzi_vc_issuer/ura_issuer_test.go
+++ b/uzi_vc_issuer/ura_issuer_test.go
@@ -2,105 +2,107 @@ package uzi_vc_issuer
 
 import (
 	"crypto/rsa"
-	"crypto/sha512"
 	"crypto/x509"
 	"crypto/x509/pkix"
-	"encoding/base64"
-	"encoding/json"
-	"fmt"
-	clo "github.com/huandu/go-clone"
-	ssi "github.com/nuts-foundation/go-did"
-	"github.com/nuts-foundation/go-did/did"
-	"github.com/nuts-foundation/go-did/vc"
-	"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
-	"github.com/stretchr/testify/require"
 	"os"
-	"strings"
 	"testing"
+
+	"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestBuildUraVerifiableCredential(t *testing.T) {
 
-	_certs, _, _, privateKey, signingCert, err := x509_cert.BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380")
-	failError(t, err)
+	chainBytes, err := os.ReadFile("testdata/valid_chain.pem")
+	require.NoError(t, err, "failed to read chain")
+
+	type inFn = func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string)
+
+	defaultIn := func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+		pemBlocks, err := parsePemBytes(chainBytes)
+		require.NoError(t, err, "failed to parse pem blocks")
+
+		certs, err := parseCertificatesFromPemBlocks(pemBlocks)
+		require.NoError(t, err, "failed to parse certificates from pem blocks")
+
+		privKey, err := NewPrivateKey("testdata/signing_key.pem")
+		require.NoError(t, err, "failed to read signing key")
+
+		return certs, privKey, "did:example:123"
+	}
 
 	tests := []struct {
 		name      string
-		in        func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string)
+		in        inFn
 		errorText string
 	}{
 		{
-			name: "empty chain",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				return []*x509.Certificate{}, privateKey, "did:example:123"
-			},
-			errorText: "empty certificate chain",
+			name:      "ok - valid chain",
+			in:        defaultIn,
+			errorText: "",
 		},
+		// {
+		// 	name: "nok - empty chain",
+		// 	in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+		// 		_, privKey, didStr := defaultIn(t)
+		// 		return []*x509.Certificate{}, privKey, didStr
+		// 	},
+		// 	errorText: "empty certificate chain",
+		// },
 		{
-			name: "invalid signing certificate 1",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				signingTmpl, err := x509_cert.SigningCertTemplate(nil, "2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380")
-				signingTmpl.Subject.SerialNumber = "KAAS"
-				failError(t, err)
-				cert, _, err := x509_cert.CreateCert(signingTmpl, signingCert, signingCert.PublicKey, privateKey)
-				failError(t, err)
-				certs[0] = cert
-				return certs, privateKey, "did:example:123"
+			name: "nok - empty serial number",
+			in: func(*testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+				certs, privKey, didStr := defaultIn(t)
+				certs[0].Subject.SerialNumber = ""
+				return certs, privKey, didStr
 			},
-			errorText: "serial number does not match UZI number",
+			errorText: "serialNumber not found in signing certificate",
 		},
 		{
-			name: "invalid signing certificate 2",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				signingTmpl, err := x509_cert.SigningCertTemplate(nil, "2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380")
-				signingTmpl.ExtraExtensions = make([]pkix.Extension, 0)
-				failError(t, err)
-				cert, _, err := x509_cert.CreateCert(signingTmpl, signingCert, signingCert.PublicKey, privateKey)
-				failError(t, err)
-				certs[0] = cert
-				return certs, privateKey, "did:example:123"
+			name: "nok - invalid signing serial in signing cert",
+			in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+				certs, privKey, didStr := defaultIn(t)
+
+				certs[0].Subject.SerialNumber = "invalid-serial-number"
+				return certs, privKey, didStr
 			},
-			errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate",
+			errorText: "serial number does not match UZI number",
 		},
 		{
-			name: "invalid serial number",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				certificate := clo.Clone(certs[0]).(*x509.Certificate)
-				certificate.Subject.SerialNumber = ""
-				certs[0] = certificate
-				return certs, privateKey, "did:example:123"
+			name: "nok - invalid signing certificate 2",
+			in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+				certs, privKey, didStr := defaultIn(t)
+
+				certs[0].ExtraExtensions = make([]pkix.Extension, 0)
+				certs[0].Extensions = make([]pkix.Extension, 0)
+				return certs, privKey, didStr
 			},
-			errorText: "serialNumber not found in signing certificate",
+			errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate",
 		},
 		{
-			name: "broken cert",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+			name: "nok - empty cert in chain",
+			in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+				certs, privKey, didStr := defaultIn(t)
 				certs[0] = &x509.Certificate{}
-				return certs, privateKey, "did:example:123"
+				return certs, privKey, didStr
 			},
 			errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate",
 		},
-		{
-			name: "broken signing key",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				return certs, nil, "did:example:123"
-			},
-			errorText: "signing key is nil",
-		},
-		{
-			name: "happy path",
-			in: func(certs []*x509.Certificate) ([]*x509.Certificate, *rsa.PrivateKey, string) {
-				return certs, privateKey, "did:example:123"
-			},
-			errorText: "",
-		},
+		// {
+		// 	name: "nok - nil signing key",
+		// 	in: func(t *testing.T) ([]*x509.Certificate, *rsa.PrivateKey, string) {
+		// 		certs, _, didStr := defaultIn(t)
+		// 		return certs, nil, didStr
+		// 	},
+		// 	errorText: "signing key is nil",
+		// },
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			certificates := clo.Clone(_certs).([]*x509.Certificate)
-			certificates, signingKey, subjectDID := tt.in(certificates)
-			_, err := BuildUraVerifiableCredential(certificates, signingKey, subjectDID, []x509_cert.SubjectTypeName{})
+			certificates, signingKey, subject := tt.in(t)
+			_, err := Issue(certificates, signingKey, subjectDID(subject))
 			if err != nil {
 				if err.Error() != tt.errorText {
 					t.Errorf("BuildUraVerifiableCredential() error = '%v', wantErr '%v'", err.Error(), tt.errorText)
@@ -112,9 +114,132 @@ func TestBuildUraVerifiableCredential(t *testing.T) {
 	}
 }
 
-func TestBuildCertificateChain(t *testing.T) {
-	certs, _, _, _, _, err := x509_cert.BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380")
-	failError(t, err)
+func TestNewFileName(t *testing.T) {
+	// Create a temporary file for testing
+	tmpFile, err := os.CreateTemp("", "testfile")
+	if err != nil {
+		t.Fatalf("Failed to create temp file: %v", err)
+	}
+	defer os.Remove(tmpFile.Name())
+
+	tests := []struct {
+		name      string
+		fileName  string
+		expectErr bool
+	}{
+		{"ValidFile", tmpFile.Name(), false},
+		{"InvalidFile", "nonexistentfile", true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := newFileName(tt.fileName)
+			if (err != nil) != tt.expectErr {
+				t.Errorf("newFileName() error = %v, expectErr %v", err, tt.expectErr)
+			}
+		})
+	}
+}
+
+func TestReadFile(t *testing.T) {
+	// Create a temporary file
+	tmpfile, err := os.CreateTemp("", "example")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.Remove(tmpfile.Name()) // clean up
+
+	content := []byte("Hello, World!")
+	if _, err := tmpfile.Write(content); err != nil {
+		t.Fatal(err)
+	}
+	if err := tmpfile.Close(); err != nil {
+		t.Fatal(err)
+	}
+
+	// Call readFile function
+	fileName := fileName(tmpfile.Name())
+	readContent, err := readFile(fileName)
+	if err != nil {
+		t.Fatalf("readFile() error = %v", err)
+	}
+
+	// Assert the content matches
+	if string(readContent) != string(content) {
+		t.Errorf("readFile() = %v, want %v", string(readContent), string(content))
+	}
+}
+
+func TestIssue(t *testing.T) {
+	validChain, err := NewValidCertificateChain("testdata/valid_chain.pem")
+	require.NoError(t, err, "failed to read chain")
+
+	validKey, err := NewPrivateKey("testdata/signing_key.pem")
+	require.NoError(t, err, "failed to read signing key")
+
+	t.Run("ok - happy path", func(t *testing.T) {
+		vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization))
+
+		require.NoError(t, err, "failed to issue verifiable credential")
+		require.NotNil(t, vc, "verifiable credential is nil")
+
+		assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String())
+		assert.Equal(t, "VerifiableCredential", vc.Type[0].String())
+		assert.Equal(t, "UziServerCertificateCredential", vc.Type[1].String())
+		assert.Equal(t, "did:x509:0:sha512:0OXDVLevEnf_sE-Ayopm0Yof_gmBwxwKZmzbDhKeAwj9vcsI_Q14TBArYsCftQTABLM-Vx9BB6zI05Me2aksaA::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare", vc.Issuer.String())
+
+		expectedCredentialSubject := []interface{}([]interface{}{map[string]interface{}{
+			"id":                           "did:example:123",
+			"O":                            "FauxCare",
+			"otherName":                    "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333",
+			"permanentIdentifier.assigner": "2.16.528.1.1007.3.3",
+			"permanentIdentifier.value":    "2222222",
+		}})
+
+		assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject)
+
+		assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate, "expiration date of VC must match signing certificate")
+	})
+}
+
+func TestParsePemBytes(t *testing.T) {
+	chainBytes, err := os.ReadFile("testdata/valid_chain.pem")
+	require.NoError(t, err, "failed to read chain")
+
+	tests := []struct {
+		name            string
+		pemBytes        []byte
+		expectNumBlocks int
+		expectErr       bool
+	}{
+		{"ValidChain", chainBytes, 4, false},
+		{"InvalidChain", []byte("invalid pem"), 0, true},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			blocks, err := parsePemBytes(tt.pemBytes)
+			if (err != nil) != tt.expectErr {
+				t.Errorf("parsePemBytes() error = %v, expectErr %v", err, tt.expectErr)
+			}
+
+			if len(blocks) != tt.expectNumBlocks {
+				t.Errorf("parsePemBytes() = %v, want %v", len(blocks), tt.expectNumBlocks)
+			}
+		})
+	}
+}
+
+func TestNewCertificateChain(t *testing.T) {
+	chainBytes, err := os.ReadFile("testdata/valid_chain.pem")
+	require.NoError(t, err, "failed to read chain")
+
+	pemBlocks, err := parsePemBytes(chainBytes)
+	require.NoError(t, err, "failed to parse pem blocks")
+
+	certs, err := parseCertificatesFromPemBlocks(pemBlocks)
+	require.NoError(t, err, "failed to parse certificates from pem blocks")
+
 	tests := []struct {
 		name      string
 		errorText string
@@ -122,7 +247,7 @@ func TestBuildCertificateChain(t *testing.T) {
 		out       func(certs []*x509.Certificate) []*x509.Certificate
 	}{
 		{
-			name: "happy flow",
+			name: "ok - valid cert input",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
 				return certs
 			},
@@ -132,31 +257,31 @@ func TestBuildCertificateChain(t *testing.T) {
 			errorText: "",
 		},
 		{
-			name: "no signing certificate",
+			name: "ok - it handles out of order certificates",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				certs = certs[1:]
+				certs = []*x509.Certificate{certs[2], certs[0], certs[3], certs[1]}
 				return certs
 			},
 			out: func(certs []*x509.Certificate) []*x509.Certificate {
-				return nil
+				return certs
 			},
-			errorText: "failed to find signing certificate",
+			errorText: "",
 		},
 		{
-			name: "no root CA certificate",
+			name: "nok - missing signing certificate",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				certs = certs[:3]
+				certs = certs[1:]
 				return certs
 			},
 			out: func(certs []*x509.Certificate) []*x509.Certificate {
 				return nil
 			},
-			errorText: "failed to find path from signingCert to root",
+			errorText: "failed to find signing certificate",
 		},
 		{
-			name: "no intermediate CA certificate type 1",
+			name: "nok - missing root CA certificate",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				certs = []*x509.Certificate{certs[0], certs[2], certs[3]}
+				certs = certs[:3]
 				return certs
 			},
 			out: func(certs []*x509.Certificate) []*x509.Certificate {
@@ -165,9 +290,9 @@ func TestBuildCertificateChain(t *testing.T) {
 			errorText: "failed to find path from signingCert to root",
 		},
 		{
-			name: "no intermediate CA certificate type 2",
+			name: "nok - missing first intermediate CA certificate",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				certs = []*x509.Certificate{certs[0], certs[1], certs[3]}
+				certs = []*x509.Certificate{certs[0], certs[2], certs[3]}
 				return certs
 			},
 			out: func(certs []*x509.Certificate) []*x509.Certificate {
@@ -176,9 +301,9 @@ func TestBuildCertificateChain(t *testing.T) {
 			errorText: "failed to find path from signingCert to root",
 		},
 		{
-			name: "no intermediate CA certificate type 3",
+			name: "nok - missing second intermediate CA certificate",
 			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				certs = []*x509.Certificate{certs[0], nil, certs[2], certs[3]}
+				certs = []*x509.Certificate{certs[0], certs[1], certs[3]}
 				return certs
 			},
 			out: func(certs []*x509.Certificate) []*x509.Certificate {
@@ -186,26 +311,12 @@ func TestBuildCertificateChain(t *testing.T) {
 			},
 			errorText: "failed to find path from signingCert to root",
 		},
-		{
-			name: "reverse certificate order",
-			in: func(certs []*x509.Certificate) []*x509.Certificate {
-				rv := make([]*x509.Certificate, 0)
-				for i := len(certs) - 1; i >= 0; i-- {
-					rv = append(rv, certs[i])
-				}
-				return rv
-			},
-			out: func(certs []*x509.Certificate) []*x509.Certificate {
-				return certs
-			},
-			errorText: "",
-		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			inputCerts := tt.in(clo.Clone(certs).([]*x509.Certificate))
-			expectedCerts := tt.out(clo.Clone(certs).([]*x509.Certificate))
-			resultCerts, err := BuildCertificateChain(inputCerts)
+			inputCerts := tt.in(certs)
+			expectedCerts := tt.out(certs)
+			resultCerts, err := newCertificateChain(inputCerts)
 			if err != nil {
 				if err.Error() != tt.errorText {
 					t.Errorf("BuildCertificateChain() error = '%v', wantErr '%v'", err.Error(), tt.errorText)
@@ -225,138 +336,3 @@ func TestBuildCertificateChain(t *testing.T) {
 		})
 	}
 }
-
-func TestIssue(t *testing.T) {
-
-	brokenChain, _, _, _, _, err := x509_cert.BuildSelfSignedCertChain("KAAS", "HAM")
-	failError(t, err)
-	identifier := "2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344"
-	ura := "90000380"
-	chain, _, rootCert, privKey, signingCert, err := x509_cert.BuildSelfSignedCertChain(identifier, ura)
-	bytesRootHash := sha512.Sum512(rootCert.Raw)
-	rootHash := base64.RawURLEncoding.EncodeToString(bytesRootHash[:])
-	failError(t, err)
-
-	chainPems, err := x509_cert.EncodeCertificates(chain...)
-	failError(t, err)
-	siglePem, err := x509_cert.EncodeCertificates(chain[0])
-	failError(t, err)
-	brokenPem, err := x509_cert.EncodeCertificates(brokenChain...)
-	failError(t, err)
-	signingKeyPem, err := x509_cert.EncodeRSAPrivateKey(privKey)
-	failError(t, err)
-
-	pemFile, err := os.CreateTemp(t.TempDir(), "chain.pem")
-	failError(t, err)
-	err = os.WriteFile(pemFile.Name(), chainPems, 0644)
-	failError(t, err)
-
-	brokenPemFile, err := os.CreateTemp(t.TempDir(), "broken_chain.pem")
-	failError(t, err)
-	err = os.WriteFile(brokenPemFile.Name(), brokenPem, 0644)
-	failError(t, err)
-
-	signlePemFile, err := os.CreateTemp(t.TempDir(), "single_chain.pem")
-	failError(t, err)
-	err = os.WriteFile(signlePemFile.Name(), siglePem, 0644)
-	failError(t, err)
-
-	keyFile, err := os.CreateTemp(t.TempDir(), "signing_key.pem")
-	failError(t, err)
-	err = os.WriteFile(keyFile.Name(), signingKeyPem, 0644)
-	failError(t, err)
-
-	emptyFile, err := os.CreateTemp(t.TempDir(), "empty.pem")
-	failError(t, err)
-	err = os.WriteFile(emptyFile.Name(), []byte{}, 0644)
-	failError(t, err)
-
-	tests := []struct {
-		name       string
-		certFile   string
-		keyFile    string
-		subjectDID string
-		allowTest  bool
-		out        *vc.VerifiableCredential
-		errorText  string
-	}{
-		{
-			name:       "happy path",
-			certFile:   pemFile.Name(),
-			keyFile:    keyFile.Name(),
-			subjectDID: "did:example:123",
-			allowTest:  true,
-			out: &vc.VerifiableCredential{
-				Context:        []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")},
-				Issuer:         did.MustParseDID(fmt.Sprintf("did:x509:0:sha512:%s::san:otherName:%s::san:permanentIdentifier.value:%s::san:permanentIdentifier.assigner:%s", rootHash, identifier, ura, x509_cert.UraAssigner.String())).URI(),
-				Type:           []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UziServerCertificateCredential")},
-				ExpirationDate: toPtr(signingCert.NotAfter),
-			},
-			errorText: "",
-		},
-		{
-			name:       "no signing keys found",
-			certFile:   pemFile.Name(),
-			keyFile:    emptyFile.Name(),
-			subjectDID: "did:example:123",
-			allowTest:  true,
-			out:        nil,
-			errorText:  "no signing keys found",
-		},
-		{
-			name:       "invalid signing cert",
-			certFile:   signlePemFile.Name(),
-			keyFile:    keyFile.Name(),
-			subjectDID: "did:example:123",
-			allowTest:  true,
-			out:        nil,
-			errorText:  "failed to find path from signingCert to root",
-		},
-		{
-			name:       "invalid otherName",
-			certFile:   brokenPemFile.Name(),
-			keyFile:    keyFile.Name(),
-			subjectDID: "did:example:123",
-			allowTest:  true,
-			out:        nil,
-			errorText:  "failed to parse URA from OtherNameValue",
-		},
-		/* more test cases */
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			result, err := Issue(tt.certFile, tt.keyFile, tt.subjectDID, tt.allowTest, true, make([]x509_cert.SubjectTypeName, 0))
-			if err != nil {
-				if err.Error() != tt.errorText {
-					t.Errorf("Issue() error = '%v', wantErr '%v'", err.Error(), tt.errorText)
-				}
-			} else if err == nil && tt.errorText != "" {
-				t.Errorf("Issue() unexpected success, want error")
-			} else if err == nil {
-				found := vc.VerifiableCredential{}
-				err = json.Unmarshal([]byte("\""+result+"\""), &found)
-				failError(t, err)
-				compare(t, tt.out, &found)
-			}
-		})
-	}
-}
-
-func failError(t *testing.T, err error) {
-	if err != nil {
-		t.Errorf("an error occured: %v", err.Error())
-		t.Fatal(err)
-	}
-}
-
-func compare(t *testing.T, expected *vc.VerifiableCredential, found *vc.VerifiableCredential) {
-	require.True(t, strings.HasPrefix(found.ID.String(), found.Issuer.String()+"#"), "credential ID must be in form <issuer DID>#<uuid>")
-	require.Equal(t, expected.Issuer.String(), found.Issuer.String(), "credential issuer mismatch")
-	require.Equal(t, expected.Type, found.Type, "credential type mismatch")
-	require.Equal(t, expected.ExpirationDate, found.ExpirationDate, "credential expiration date mismatch")
-}
-
-func toPtr[T any](v T) *T {
-	return &v
-}