diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..29ac881 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,26 @@ +version: "2" +checks: + # Go returns 'error's rather than throwing exceptions, so the safer the code the more errors are returned. + # To avoid deeply nested if-statements we generally use guard clauses as recommended by Martin Fowler. + # Guard statements are if-statements that, as early as possible, check conditions and return if failed. They improve readability + # over nested if-statements. This however leads to many return statements which can't be avoided, so we'll disable the max. exit points check. + return-statements: + enabled: false +exclude_patterns: + - "**/generated.go" + - "**/test/**/*.go" + - "**/test.go" + - "**/*_test.go" + - "**/test*.go" + - "**/mock/**/*.go" + - "**/mock.go" + - "**/*_mock.go" + - "docs/**/*.go" + - "codegen/**/*.go" + - "**/*.pb.go" + - "e2e-tests/**/*.go" +plugins: + gofmt: + enabled: true + govet: + enabled: true diff --git a/.gitignore b/.gitignore index 997522f..3e2275e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pem uzi-did-x509-issuer +c.out diff --git a/did_x509/did_x509.go b/did_x509/did_x509.go index 1738740..db55691 100644 --- a/did_x509/did_x509.go +++ b/did_x509/did_x509.go @@ -15,13 +15,12 @@ type X509Did struct { Version string RootCertificateHash string RootCertificateHashAlg string - Ura string - SanType x509_cert.SanTypeName + Policies []*x509_cert.OtherNameValue } // FormatDid constructs a decentralized identifier (DID) from a certificate chain and an optional policy. // It returns the formatted DID string or an error if the root certificate or hash calculation fails. -func FormatDid(caCert *x509.Certificate, policy string) (string, error) { +func FormatDid(caCert *x509.Certificate, policy ...string) (string, error) { alg := "sha512" rootHash, err := x509_cert.Hash(caCert.Raw, alg) if err != nil { @@ -29,22 +28,19 @@ func FormatDid(caCert *x509.Certificate, policy string) (string, error) { } encodeToString := base64.RawURLEncoding.EncodeToString(rootHash) fragments := []string{"did", "x509", "0", alg, encodeToString} - if policy != "" { - return strings.Join([]string{strings.Join(fragments, ":"), policy}, "::"), nil - } - return strings.Join(fragments, ":"), nil + return strings.Join([]string{strings.Join(fragments, ":"), strings.Join(policy, "::")}, "::"), nil } // CreateDid generates a Decentralized Identifier (DID) from a given certificate chain. // It extracts the Unique Registration Address (URA) from the chain, creates a policy with it, and formats the DID. // Returns the generated DID or an error if any step fails. func CreateDid(signingCert, caCert *x509.Certificate) (string, error) { - otherNameValue, sanType, err := x509_cert.FindOtherName(signingCert) + otherNames, err := x509_cert.FindSanTypes(signingCert) if err != nil { return "", err } - policy := CreatePolicy(otherNameValue, sanType) - formattedDid, err := FormatDid(caCert, policy) + policies := CreatePolicies(otherNames) + formattedDid, err := FormatDid(caCert, policies...) return formattedDid, err } func ParseDid(didString string) (*X509Did, error) { @@ -53,25 +49,48 @@ func ParseDid(didString string) (*X509Did, error) { if didObj.Method != "x509" { return nil, errors.New("invalid didString method") } - regex := regexp.MustCompile(`0:(\w+):([^:]+)::san:([^:]+):(.+)`) - submatch := regex.FindStringSubmatch(didObj.ID) - if len(submatch) != 5 { + fullIdString := didObj.ID + idParts := strings.Split(fullIdString, "::") + if len(idParts) < 2 { + return nil, errors.New("invalid didString format, expected did:x509:0:alg:hash::(san:type:ura)+") + } + rootIdString := idParts[0] + policyParsString := idParts[1:] + regex := regexp.MustCompile(`0:(\w+):([^:]+)`) + submatch := regex.FindStringSubmatch(rootIdString) + if len(submatch) != 3 { return nil, errors.New("invalid didString format, expected didString:x509:0:alg:hash::san:type:ura") } x509Did.Version = "0" x509Did.RootCertificateHashAlg = submatch[1] x509Did.RootCertificateHash = submatch[2] - x509Did.SanType = x509_cert.SanTypeName(submatch[3]) - x509Did.Ura = submatch[4] + + for _, policyString := range policyParsString { + regex := regexp.MustCompile(`(\w+):([^:]+):([^:]+)`) + submatch := regex.FindStringSubmatch(policyString) + if len(submatch) != 4 { + return nil, errors.New("invalid didString format, expected didString:x509:0:alg:hash::san:type:ura") + } + x509Did.Policies = append(x509Did.Policies, &x509_cert.OtherNameValue{ + PolicyType: x509_cert.PolicyType(submatch[1]), + Type: x509_cert.SanTypeName(submatch[2]), + Value: submatch[3], + }) + } + return &x509Did, nil } -// CreatePolicy constructs a policy string using the provided URA, fixed string "san", and "permanentIdentifier". +// CreatePolicies constructs a policy string using the provided URA, fixed string "san", and "permanentIdentifier". // It joins these components with colons and returns the resulting policy string. -func CreatePolicy(otherNameValue string, sanType x509_cert.SanTypeName) string { - fragments := []string{"san", string(sanType), otherNameValue} - policy := strings.Join(fragments, ":") - return policy +func CreatePolicies(otherNames []*x509_cert.OtherNameValue) []string { + var policies []string + for _, otherName := range otherNames { + fragments := []string{string(otherName.PolicyType), string(otherName.Type), otherName.Value} + policy := strings.Join(fragments, ":") + policies = append(policies, policy) + } + return policies } // FindRootCertificate traverses a chain of x509 certificates and returns the first certificate that is a CA. diff --git a/did_x509/did_x509_test.go b/did_x509/did_x509_test.go index 4f644e4..ce090c5 100644 --- a/did_x509/did_x509_test.go +++ b/did_x509/did_x509_test.go @@ -4,19 +4,20 @@ import ( "crypto/x509" "encoding/base64" "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" + "reflect" "strings" "testing" ) // TestDefaultDidCreator_CreateDid tests the CreateDid function of DefaultDidProcessor by providing different certificate chains. // It checks for correct DID generation and appropriate error messages. -func TestDefaultDidCreator_CreateDid(t *testing.T) { +func TestDefaultDidCreator_CreateDidSingle(t *testing.T) { type fields struct { } type args struct { chain []*x509.Certificate } - chain, _, rootCert, _, _, err := x509_cert.BuildSelfSignedCertChain("A_BIG_STRING") + chain, _, rootCert, _, _, err := x509_cert.BuildSelfSignedCertChain("A_BIG_STRING", "") if err != nil { t.Fatal(err) } @@ -61,10 +62,68 @@ func TestDefaultDidCreator_CreateDid(t *testing.T) { }) } } +func TestDefaultDidCreator_CreateDidDouble(t *testing.T) { + type fields struct { + } + type args struct { + chain []*x509.Certificate + } + chain, _, rootCert, _, _, err := x509_cert.BuildSelfSignedCertChain("A_BIG_STRING", "A_SMALL_STRING") + if err != nil { + t.Fatal(err) + } + + alg := "sha512" + hash, err := x509_cert.Hash(rootCert.Raw, alg) + if err != nil { + t.Fatal(err) + } + rootHashString := base64.RawURLEncoding.EncodeToString(hash) + tests := []struct { + name string + fields fields + args args + want string + errMsg string + }{ + { + name: "Happy path", + fields: fields{}, + args: args{chain: chain}, + want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "san", "permanentIdentifier.value", "A_SMALL_STRING", "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":"), + errMsg: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateDid(tt.args.chain[0], tt.args.chain[len(tt.args.chain)-1]) + wantErr := tt.errMsg != "" + if (err != nil) != wantErr { + t.Errorf("DefaultDidProcessor.CreateDid() error = %v, errMsg %v", err, tt.errMsg) + return + } else if wantErr { + if err.Error() != tt.errMsg { + t.Errorf("DefaultDidProcessor.CreateDid() expected = \"%v\", got: \"%v\"", tt.errMsg, err.Error()) + } + } + + if got != tt.want { + t.Errorf("DefaultDidProcessor.CreateDid() = \n%v\n, want: \n%v\n", got, tt.want) + } + }) + } +} // TestDefaultDidCreator_ParseDid tests the ParseDid function of DefaultDidProcessor by providing different DID strings. // It checks for correct X509Did parsing and appropriate error messages. func TestDefaultDidCreator_ParseDid(t *testing.T) { + policies := []*x509_cert.OtherNameValue{ + { + PolicyType: "san", + Type: "otherName", + Value: "A_BIG_STRING", + }, + } type fields struct { } type args struct { @@ -95,7 +154,7 @@ func TestDefaultDidCreator_ParseDid(t *testing.T) { name: "Happy path", fields: fields{}, args: args{didString: "did:x509:0:sha512:hash::san:otherName:A_BIG_STRING"}, - want: &X509Did{Version: "0", RootCertificateHashAlg: "sha512", RootCertificateHash: "hash", SanType: "otherName", Ura: "A_BIG_STRING"}, + want: &X509Did{Version: "0", RootCertificateHashAlg: "sha512", RootCertificateHash: "hash", Policies: policies}, errMsg: "", }, } @@ -116,8 +175,7 @@ func TestDefaultDidCreator_ParseDid(t *testing.T) { (tt.want.Version != got.Version || tt.want.RootCertificateHashAlg != got.RootCertificateHashAlg || tt.want.RootCertificateHash != got.RootCertificateHash || - tt.want.SanType != got.SanType || - tt.want.Ura != got.Ura) { + !reflect.DeepEqual(tt.want.Policies, got.Policies)) { t.Errorf("DefaultDidProcessor.ParseDid() = %v, want = %v", got, tt.want) } }) diff --git a/go.mod b/go.mod index 0983813..983a1c8 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/nuts-foundation/uzi-did-x509-issuer go 1.23.1 require ( - github.com/alecthomas/kong v1.2.1 + github.com/alecthomas/kong v1.3.0 github.com/google/uuid v1.6.0 github.com/huandu/go-clone v1.7.2 - github.com/lestrrat-go/jwx/v2 v2.1.1 + 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 golang.org/x/crypto v0.28.0 diff --git a/go.sum b/go.sum index cfa0a8c..29ccd6d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= -github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= -github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.3.0 h1:YJKuU6/TV2XOBtymafSeuzDvLAFR8cYMZiXVNLhAO6g= +github.com/alecthomas/kong v1.3.0/go.mod h1:IDc8HyiouDdpdiEiY81iaEJM8rSIW6LzX8On4FCO0bE= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,8 +27,8 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E= -github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0= +github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc= +github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/mr-tron/base58 v1.1.0 h1:Y51FGVJ91WBqCEabAi5OPUz38eAx8DakuAm5svLcsfQ= diff --git a/main.go b/main.go index a3a8ac1..77f11ca 100644 --- a/main.go +++ b/main.go @@ -54,7 +54,7 @@ func main() { // 2.16.528.1.1007.99.2110-1--S--00.000- otherName := fmt.Sprintf("2.16.528.1.1007.99.2110-1-%s-S-%s-00.000-%s", cli.TestCert.Uzi, cli.TestCert.Ura, cli.TestCert.Agb) fmt.Println("Building certificate chain for identifier:", otherName) - chain, _, _, privKey, _, err := x509_cert.BuildSelfSignedCertChain(otherName) + chain, _, _, privKey, _, err := x509_cert.BuildSelfSignedCertChain(otherName, cli.TestCert.Ura) if err != nil { fmt.Println(err) os.Exit(-1) diff --git a/pem/pem_reader_test.go b/pem/pem_reader_test.go index 73aeec7..b41a4c3 100644 --- a/pem/pem_reader_test.go +++ b/pem/pem_reader_test.go @@ -59,7 +59,7 @@ func TestParseFileOrPath(t *testing.T) { log.Fatal(err) } }(file.Name()) - certs, chainPem, _, _, _, err := x509_cert.BuildSelfSignedCertChain("A BIG STRING") + certs, chainPem, _, _, _, err := x509_cert.BuildSelfSignedCertChain("A BIG STRING", "a small one") failError(t, err) for i := 0; i < chainPem.Len(); i++ { certBlock, ok := chainPem.Get(i) @@ -86,7 +86,7 @@ func TestParseFileOrPath(t *testing.T) { }) t.Run("Happy flow directory", func(t *testing.T) { - certs, chainPem, _, _, _, err := x509_cert.BuildSelfSignedCertChain("A BIG STRING") + certs, chainPem, _, _, _, err := x509_cert.BuildSelfSignedCertChain("A BIG STRING", "a small one") failError(t, err) tempDir, _ := os.MkdirTemp("", "example") defer func(path string) { diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go index 6d7b522..8ca64c0 100644 --- a/uzi_vc_issuer/ura_issuer.go +++ b/uzi_vc_issuer/ura_issuer.go @@ -106,18 +106,19 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri if serialNumber == "" { return nil, errors.New("serialNumber not found in signing certificate") } - otherNameValue, _, err := x509_cert.FindOtherName(signingCert) + otherNameValues, err := x509_cert.FindSanTypes(signingCert) if err != nil { return nil, err } - uzi, _, _, err := x509_cert.ParseUraFromOtherNameValue(otherNameValue) + stringValue, err := x509_cert.FindOtherNameValue(otherNameValues, x509_cert.PolicyTypeSan, x509_cert.SanTypeOtherName) + uzi, _, _, err := x509_cert.ParseUraFromOtherNameValue(stringValue) if err != nil { return nil, err } if uzi != serialNumber { return nil, errors.New("serial number does not match UZI number") } - template, err := uraCredential(did, otherNameValue, subjectDID) + template, err := uraCredential(did, otherNameValues, subjectDID) if err != nil { return nil, err } @@ -262,21 +263,23 @@ 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, otherNameValue string, subjectDID string) (*vc.VerifiableCredential, error) { +func uraCredential(issuer string, otherNameValues []*x509_cert.OtherNameValue, subjectDID string) (*vc.VerifiableCredential, error) { exp := time.Now().Add(time.Hour * 24 * 365 * 100) iat := time.Now() + subject := map[x509_cert.SanTypeName]interface{}{ + "id": subjectDID, + } + for _, otherNameValue := range otherNameValues { + subject[otherNameValue.Type] = otherNameValue.Value + } + return &vc.VerifiableCredential{ - Issuer: ssi.MustParseURI(issuer), - Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, - Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UziServerCertificateCredential")}, - ID: func() *ssi.URI { id := ssi.MustParseURI(uuid.NewString()); return &id }(), - IssuanceDate: iat, - ExpirationDate: &exp, - CredentialSubject: []interface{}{ - map[string]interface{}{ - "id": subjectDID, - "otherName": otherNameValue, - }, - }, + Issuer: ssi.MustParseURI(issuer), + Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, + Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI("UziServerCertificateCredential")}, + ID: func() *ssi.URI { id := ssi.MustParseURI(uuid.NewString()); return &id }(), + IssuanceDate: iat, + ExpirationDate: &exp, + CredentialSubject: []interface{}{subject}, }, nil } diff --git a/uzi_vc_issuer/ura_issuer_test.go b/uzi_vc_issuer/ura_issuer_test.go index 5bc80b2..a9d4cfb 100644 --- a/uzi_vc_issuer/ura_issuer_test.go +++ b/uzi_vc_issuer/ura_issuer_test.go @@ -20,7 +20,7 @@ import ( 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") + _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) tests := []struct { @@ -38,7 +38,7 @@ func TestBuildUraVerifiableCredential(t *testing.T) { { 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") + 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) @@ -51,7 +51,7 @@ func TestBuildUraVerifiableCredential(t *testing.T) { { 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") + 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) @@ -59,7 +59,7 @@ func TestBuildUraVerifiableCredential(t *testing.T) { certs[0] = cert return certs, privateKey, "did:example:123" }, - errorText: "no otherName found in the SAN attributes, please check if the certificate is an UZI Server Certificate", + errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", }, { name: "invalid serial number", @@ -77,7 +77,7 @@ func TestBuildUraVerifiableCredential(t *testing.T) { certs[0] = &x509.Certificate{} return certs, privateKey, "did:example:123" }, - errorText: "no otherName found in the SAN attributes, please check if the certificate is an UZI Server Certificate", + errorText: "no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate", }, { name: "broken signing key", @@ -112,7 +112,7 @@ 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") + certs, _, _, _, _, err := x509_cert.BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380") failError(t, err) tests := []struct { name string @@ -227,10 +227,11 @@ func TestBuildCertificateChain(t *testing.T) { func TestIssue(t *testing.T) { - brokenChain, _, _, _, _, err := x509_cert.BuildSelfSignedCertChain("KAAS") + 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" - chain, _, rootCert, privKey, _, err := x509_cert.BuildSelfSignedCertChain(identifier) + ura := "90000380" + chain, _, rootCert, privKey, _, err := x509_cert.BuildSelfSignedCertChain(identifier, ura) bytesRootHash := sha512.Sum512(rootCert.Raw) rootHash := base64.RawURLEncoding.EncodeToString(bytesRootHash[:]) failError(t, err) @@ -286,7 +287,7 @@ func TestIssue(t *testing.T) { 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", rootHash, identifier)).URI(), + 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")}, }, errorText: "", diff --git a/uzi_vc_validator/ura_validator.go b/uzi_vc_validator/ura_validator.go index 5951e3f..dc51a05 100644 --- a/uzi_vc_validator/ura_validator.go +++ b/uzi_vc_validator/ura_validator.go @@ -76,18 +76,27 @@ func (u UraValidatorImpl) Validate(jwtString string) error { if err != nil { return err } - ura, sanType, err := x509_cert.FindOtherName(signingCert) + otherNames, err := x509_cert.FindSanTypes(signingCert) if err != nil { return err } + for _, policy := range parseDid.Policies { + found := false + for _, otherName := range otherNames { + if otherName.Type == policy.Type && otherName.PolicyType == policy.PolicyType { + if policy.Value != otherName.Value { + return fmt.Errorf("%s value %s of policy %s in credential does not match according value in signing certificate", otherName.Type, otherName.Type, otherName.PolicyType) + } else { + found = true + break + } + } + } + if !found { + return fmt.Errorf("unable to locate a value for %s of policy %s", policy.Type, policy.PolicyType) + } - if ura != parseDid.Ura { - return fmt.Errorf("URA in credential does not match Ura in signing certificate") - } - if sanType != parseDid.SanType { - return fmt.Errorf("SanType in credential does not match SanType in signing certificate") } - return nil } diff --git a/x509_cert/x509_cert.go b/x509_cert/x509_cert.go index 24bfa1b..6b3578a 100644 --- a/x509_cert/x509_cert.go +++ b/x509_cert/x509_cert.go @@ -103,8 +103,8 @@ func FixChainHeaders(chain *cert.Chain) (*cert.Chain, error) { return rv, nil } -func ParseUraFromOtherNameValue(value string) (uzi string, ura string, agb string, err error) { - submatch := RegexOtherNameValue.FindStringSubmatch(value) +func ParseUraFromOtherNameValue(stringValue string) (uzi string, ura string, agb string, err error) { + submatch := RegexOtherNameValue.FindStringSubmatch(stringValue) if len(submatch) < 4 { return "", "", "", errors.New("failed to parse URA from OtherNameValue") } diff --git a/x509_cert/x509_cert_test.go b/x509_cert/x509_cert_test.go index c8495ae..4f24a19 100644 --- a/x509_cert/x509_cert_test.go +++ b/x509_cert/x509_cert_test.go @@ -74,7 +74,7 @@ func TestHash(t *testing.T) { } } func TestParseChain(t *testing.T) { - _, chainPem, _, _, _, err := BuildSelfSignedCertChain("9907878") + _, chainPem, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "900030787") failError(t, err) derChains := make([][]byte, chainPem.Len()) for i := 0; i < chainPem.Len(); i++ { @@ -118,7 +118,7 @@ func TestParseChain(t *testing.T) { } func TestParsePrivateKey(t *testing.T) { - _, _, _, privateKey, _, err := BuildSelfSignedCertChain("9907878") + _, _, _, privateKey, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "900030787") failError(t, err) privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) failError(t, err) diff --git a/x509_cert/x509_test_utils.go b/x509_cert/x509_test_utils.go index 25df21c..8faabeb 100644 --- a/x509_cert/x509_test_utils.go +++ b/x509_cert/x509_test_utils.go @@ -40,7 +40,7 @@ func EncodeCertificates(certs ...*x509.Certificate) ([]byte, error) { } // BuildSelfSignedCertChain generates a certificate chain, including root, intermediate, and signing certificates. -func BuildSelfSignedCertChain(identifier string) (chain []*x509.Certificate, chainPems *cert.Chain, rootCert *x509.Certificate, signingKey *rsa.PrivateKey, signingCert *x509.Certificate, err error) { +func BuildSelfSignedCertChain(identifier string, permanentIdentifierValue string) (chain []*x509.Certificate, chainPems *cert.Chain, rootCert *x509.Certificate, signingKey *rsa.PrivateKey, signingCert *x509.Certificate, err error) { rootKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, nil, nil, nil, err @@ -84,7 +84,7 @@ func BuildSelfSignedCertChain(identifier string) (chain []*x509.Certificate, cha if err != nil { return nil, nil, nil, nil, nil, err } - signingTmpl, err := SigningCertTemplate(nil, identifier) + signingTmpl, err := SigningCertTemplate(nil, identifier, permanentIdentifierValue) if err != nil { return nil, nil, nil, nil, nil, err } @@ -134,7 +134,7 @@ func CertTemplate(serialNumber *big.Int, organization string) (*x509.Certificate } // SigningCertTemplate creates a x509.Certificate template for a signing certificate with an optional serial number. -func SigningCertTemplate(serialNumber *big.Int, identifier string) (*x509.Certificate, error) { +func SigningCertTemplate(serialNumber *big.Int, identifier string, permanentIdentifierValue string) (*x509.Certificate, error) { // generate a random serial number (a real cert authority would have some logic behind this) if serialNumber == nil { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 8) @@ -160,6 +160,31 @@ func SigningCertTemplate(serialNumber *big.Int, identifier string) (*x509.Certif } var list []asn1.RawValue list = append(list, *raw) + + if permanentIdentifierValue != "" { + permId := StingAndOid{ + Value: permanentIdentifierValue, + Assigner: UraAssigner, + } + raw, err = toRawValue(permId, "seq") + if err != nil { + return nil, err + } + permOtherName := OtherName{ + TypeID: PermanentIdentifierType, + Value: asn1.RawValue{ + Class: 2, + Tag: 0, + IsCompound: true, + Bytes: raw.FullBytes, + }, + } + raw, err = toRawValue(permOtherName, "tag:0") + if err != nil { + return nil, err + } + list = append(list, *raw) + } //fmt.Println("OFF") marshal, err := asn1.Marshal(list) if err != nil { diff --git a/x509_cert/x509_utils.go b/x509_cert/x509_utils.go index 523e163..f4d2690 100644 --- a/x509_cert/x509_utils.go +++ b/x509_cert/x509_utils.go @@ -14,27 +14,118 @@ type OtherName struct { Value asn1.RawValue `asn1:"tag:0,explicit"` } +type StingAndOid struct { + Value string + Assigner asn1.ObjectIdentifier +} + +type PolicyType string + +const ( + PolicyTypeSan PolicyType = "san" +) + type SanType pkix.AttributeTypeAndValue type SanTypeName string const ( - SanTypeOtherName SanTypeName = "otherName" + SanTypeOtherName SanTypeName = "otherName" + SanTypePermanentIdentifierValue SanTypeName = "permanentIdentifier.value" + SanTypePermanentIdentifierAssigner SanTypeName = "permanentIdentifier.assigner" ) -func FindOtherName(certificate *x509.Certificate) (string, SanTypeName, error) { +type OtherNameValue struct { + PolicyType PolicyType + Type SanTypeName + Value string +} + +func FindSanTypes(certificate *x509.Certificate) ([]*OtherNameValue, error) { + rv := make([]*OtherNameValue, 0) if certificate == nil { - return "", "", errors.New("certificate is nil") + return nil, errors.New("certificate is nil") } otherNameValue, err := findOtherNameValue(certificate) if err != nil { - return "", "", err + return nil, err } if otherNameValue != "" { - return otherNameValue, SanTypeOtherName, nil + rv = append(rv, &OtherNameValue{ + Value: otherNameValue, + Type: SanTypeOtherName, + PolicyType: PolicyTypeSan, + }) + } + + value, assigner, err := findPermanentIdentifiers(certificate) + if err != nil { + return nil, err + } + if value != "" { + rv = append(rv, &OtherNameValue{ + Value: value, + Type: SanTypePermanentIdentifierValue, + PolicyType: PolicyTypeSan, + }) + } + if len(assigner) > 0 { + rv = append(rv, &OtherNameValue{ + Value: assigner.String(), + Type: SanTypePermanentIdentifierAssigner, + PolicyType: PolicyTypeSan, + }) + } + if len(rv) == 0 { + err = errors.New("no values found in the SAN attributes, please check if the certificate is an UZI Server Certificate") + return nil, err + } + return rv, nil +} + +func FindOtherNameValue(value []*OtherNameValue, policyType PolicyType, sanTypeName SanTypeName) (string, error) { + for _, v := range value { + if v != nil && v.PolicyType == policyType && v.Type == sanTypeName { + return v.Value, nil + } + } + return "", fmt.Errorf("failed to find value for policyType: %s and sanTypeName: %s", policyType, sanTypeName) +} + +func findPermanentIdentifiers(cert *x509.Certificate) (string, asn1.ObjectIdentifier, error) { + value := "" + var assigner asn1.ObjectIdentifier + for _, extension := range cert.Extensions { + if extension.Id.Equal(SubjectAlternativeNameType) { + err := forEachSAN(extension.Value, func(tag int, data []byte) error { + if tag != 0 { + return nil + } + var other OtherName + _, err := asn1.UnmarshalWithParams(data, &other, "tag:0") + if err != nil { + return fmt.Errorf("could not parse requested other SAN: %v", err) + } + if other.TypeID.Equal(PermanentIdentifierType) { + var x StingAndOid + _, err = asn1.Unmarshal(other.Value.Bytes, &x) + if err != nil { + return err + } + value = x.Value + assigner = x.Assigner + + } + return nil + }) + if err != nil { + return "", nil, err + } + + return value, assigner, err + } } - err = errors.New("no otherName found in the SAN attributes, please check if the certificate is an UZI Server Certificate") - return "", "", err + return "", nil, nil } func findOtherNameValue(cert *x509.Certificate) (string, error) { diff --git a/x509_cert/x509_utils_test.go b/x509_cert/x509_utils_test.go index 5c4b016..b3a9df8 100644 --- a/x509_cert/x509_utils_test.go +++ b/x509_cert/x509_utils_test.go @@ -2,27 +2,42 @@ package x509_cert import ( "crypto/x509" + "reflect" "testing" ) func TestFindOtherName(t *testing.T) { - chain, _, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344") + chain, _, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "90000380") if err != nil { t.Fatal(err) } tests := []struct { name string certificate *x509.Certificate - wantName string - wantType SanTypeName + want []*OtherNameValue wantErr bool }{ { name: "ValidOtherName", certificate: chain[0], - wantName: "2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", - wantType: SanTypeOtherName, - wantErr: false, + want: []*OtherNameValue{ + { + PolicyType: PolicyTypeSan, + Type: SanTypeOtherName, + Value: "2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", + }, + { + PolicyType: PolicyTypeSan, + Type: SanTypePermanentIdentifierValue, + Value: "90000380", + }, + { + PolicyType: PolicyTypeSan, + Type: SanTypePermanentIdentifierAssigner, + Value: UraAssigner.String(), + }, + }, + wantErr: false, }, { name: "NoOtherName", @@ -37,16 +52,17 @@ func TestFindOtherName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotName, gotType, err := FindOtherName(tt.certificate) + otherNames, err := FindSanTypes(tt.certificate) if (err != nil) != tt.wantErr { - t.Errorf("FindOtherName() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("FindSanTypes() error = %v, wantErr %v", err, tt.wantErr) return } - if gotName != tt.wantName { - t.Errorf("FindOtherName() gotName = %v, want %v", gotName, tt.wantName) - } - if gotType != tt.wantType { - t.Errorf("FindOtherName() gotType = %v, want %v", gotType, tt.wantType) + if otherNames != nil { + if !reflect.DeepEqual(otherNames, tt.want) { + t.Errorf("FindSanTypes() got = %v, want %v", otherNames, tt.want) + } + } else if !tt.wantErr { + t.Errorf("unexpected nil from otherNames") } }) }