From 18434878bfcea5179c9fbb88e06dea05e8ba95dc Mon Sep 17 00:00:00 2001 From: Steven van der Vegt 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 #") - 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 -}