diff --git a/did_x509/did_x509.go b/did_x509/did_x509.go index 9e03065..d1a19ae 100644 --- a/did_x509/did_x509.go +++ b/did_x509/did_x509.go @@ -1,6 +1,7 @@ package did_x509 import ( + "crypto/sha256" "crypto/x509" "encoding/base64" "errors" @@ -14,6 +15,12 @@ import ( "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" ) +// hashAlg is the default hash algorithm used for hashing issuerCertificate +const hashAlg = "sha256" + +// newHashFn is the default hash function used for hashing issuerCertificate +var newHashFn = sha256.New + type X509Did struct { Version string RootCertificateHash string @@ -23,30 +30,30 @@ type X509Did struct { // 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) { - alg := "sha512" - rootHash, err := x509_cert.Hash(caCert.Raw, alg) - if err != nil { - return "", err - } - encodeToString := base64.RawURLEncoding.EncodeToString(rootHash) - fragments := []string{"did", "x509", "0", alg, encodeToString} - return strings.Join([]string{strings.Join(fragments, ":"), strings.Join(policy, "::")}, "::"), nil +func FormatDid(issuerCert *x509.Certificate, policy ...string) (*did.DID, error) { + hasher := newHashFn() + hasher.Write(issuerCert.Raw) + sum := hasher.Sum(nil) + + b64EncodedHash := base64.RawURLEncoding.EncodeToString(sum[:]) + fragments := []string{"did", "x509", "0", hashAlg, b64EncodedHash} + didString := strings.Join([]string{strings.Join(fragments, ":"), strings.Join(policy, "::")}, "::") + return did.ParseDID(didString) } // 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, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (string, error) { +func CreateDid(signingCert, caCert *x509.Certificate, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (*did.DID, error) { otherNames, err := x509_cert.SelectSanTypes(signingCert, types...) if err != nil { - return "", err + return nil, err } policies := CreateOtherNamePolicies(otherNames) subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...) if err != nil { - return "", err + return nil, err } policies = append(policies, CreateSubjectPolicies(subjectTypes)...) diff --git a/did_x509/did_x509_test.go b/did_x509/did_x509_test.go index b190945..0e22552 100644 --- a/did_x509/did_x509_test.go +++ b/did_x509/did_x509_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert" "github.com/stretchr/testify/assert" ) @@ -44,19 +45,18 @@ func TestCreateDidSingle(t *testing.T) { t.Fatal(err) } - alg := "sha512" - hash, err := x509_cert.Hash(rootCert.Raw, alg) - if err != nil { - t.Fatal(err) - } - rootHashString := base64.RawURLEncoding.EncodeToString(hash) + hash := newHashFn() + hash.Write(rootCert.Raw) + sum := hash.Sum(nil) + + rootHashString := base64.RawURLEncoding.EncodeToString(sum[:]) types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue, x509_cert.SanTypePermanentIdentifierAssigner} tests := []struct { name string fields fields args args - want string + want did.DID errMsg string sanTypes []x509_cert.SanTypeName subjectTypes []x509_cert.SubjectTypeName @@ -65,7 +65,7 @@ func TestCreateDidSingle(t *testing.T) { 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_PERMANENT_STRING", "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":"), + want: did.MustParseDID(strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING", "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":")), sanTypes: types, errMsg: "", }, @@ -73,31 +73,31 @@ func TestCreateDidSingle(t *testing.T) { 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_PERMANENT_STRING"}, ":"), + want: did.MustParseDID(strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING"}, ":")), sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue}, errMsg: "", }, { - name: "Happy path", + name: "ok - with san othername", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":"), + want: did.MustParseDID(strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":")), sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}, errMsg: "", }, { - name: "Happy path", + name: "ok - with san permanentIdentifier.value", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING"}, ":"), + want: did.MustParseDID(strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING"}, ":")), sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierValue}, errMsg: "", }, { - name: "Happy path", + name: "ok - with san permanentIdentifier.assigner", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":"), + want: did.MustParseDID(strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":")), sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierAssigner}, errMsg: "", }, @@ -115,7 +115,7 @@ func TestCreateDidSingle(t *testing.T) { } } - if got != tt.want { + if *got != tt.want { t.Errorf("DefaultDidProcessor.CreateDid() = \n%v\n, want: \n%v\n", got, tt.want) } }) @@ -132,12 +132,11 @@ func TestCreateDidDouble(t *testing.T) { t.Fatal(err) } - alg := "sha512" - hash, err := x509_cert.Hash(rootCert.Raw, alg) - if err != nil { - t.Fatal(err) - } - rootHashString := base64.RawURLEncoding.EncodeToString(hash) + hash := newHashFn() + hash.Write(rootCert.Raw) + sum := hash.Sum(nil) + + rootHashString := base64.RawURLEncoding.EncodeToString(sum[:]) sanTypeNames := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue, x509_cert.SanTypePermanentIdentifierAssigner} sanTypeNamesShort := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName} subjectTypeNamesShort := []x509_cert.SubjectTypeName{x509_cert.SubjectTypeOrganization} @@ -155,7 +154,7 @@ func TestCreateDidDouble(t *testing.T) { name: "Happy path san", 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"}, ":"), + want: strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "san", "permanentIdentifier.value", "A_SMALL_STRING", "", "san", "permanentIdentifier.assigner", "2.16.528.1.1007.3.3"}, ":"), sanTypes: sanTypeNames, errMsg: "", }, @@ -163,7 +162,7 @@ func TestCreateDidDouble(t *testing.T) { name: "Happy path short san", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":"), + want: strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":"), sanTypes: sanTypeNamesShort, errMsg: "", }, @@ -171,7 +170,7 @@ func TestCreateDidDouble(t *testing.T) { name: "Happy path short san", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "subject", "O", "FauxCare"}, ":"), + want: strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "subject", "O", "FauxCare"}, ":"), subjectTypes: subjectTypeNamesShort, errMsg: "", }, @@ -179,7 +178,7 @@ func TestCreateDidDouble(t *testing.T) { name: "Happy path mixed", fields: fields{}, args: args{chain: chain}, - want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "subject", "O", "FauxCare"}, ":"), + want: strings.Join([]string{"did", "x509", "0", hashAlg, rootHashString, "", "san", "otherName", "A_BIG_STRING", "", "subject", "O", "FauxCare"}, ":"), sanTypes: sanTypeNamesShort, subjectTypes: subjectTypeNamesShort, errMsg: "", @@ -198,7 +197,7 @@ func TestCreateDidDouble(t *testing.T) { } } - if got != tt.want { + if got != nil && got.String() != tt.want { t.Errorf("DefaultDidProcessor.CreateDid() = \n%v\n, want: \n%v\n", got, tt.want) } }) diff --git a/uzi_vc_issuer/ura_issuer.go b/uzi_vc_issuer/ura_issuer.go index 1c4f8bb..9ac9b35 100644 --- a/uzi_vc_issuer/ura_issuer.go +++ b/uzi_vc_issuer/ura_issuer.go @@ -119,6 +119,8 @@ var defaultIssueOptions = &issueOptions{ subjectAttributes: []x509_cert.SubjectTypeName{}, } +// NewValidCertificateChain reads a file and returns a valid certificate chain. +// It returns an error if the file does not exist or is empty or the certificates cannot be parsed or the chain is not valid. func NewValidCertificateChain(fileName string) (validCertificateChain, error) { certFileName, err := newFileName(fileName) @@ -148,6 +150,8 @@ func NewValidCertificateChain(fileName string) (validCertificateChain, error) { return chain, nil } +// NewPrivateKey reads a file and returns an RSA private key. +// It returns an error if the file does not exist or is empty or the key cannot be parsed. func NewPrivateKey(fileName string) (privateKey, error) { keyFileName, err := newFileName(fileName) if err != nil { @@ -172,8 +176,12 @@ func NewPrivateKey(fileName string) (privateKey, error) { return key, nil } -func NewSubjectDID(did string) (subjectDID, error) { - return subjectDID(did), nil +func NewSubjectDID(didStr string) (subjectDID, error) { + subject, err := did.ParseDID(didStr) + if err != nil { + return "", err + } + return subjectDID(subject.String()), nil } // newRSAPrivateKey parses a DER-encoded private key into an *rsa.PrivateKey. @@ -210,7 +218,7 @@ func Issue(chain validCertificateChain, key privateKey, subject subjectDID, opti types = append(types, x509_cert.SanTypePermanentIdentifierAssigner) } - did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...) + issuer, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], options.subjectAttributes, types...) if err != nil { return nil, err } @@ -236,7 +244,7 @@ func Issue(chain validCertificateChain, key privateKey, subject subjectDID, opti if uzi != serialNumber { return nil, errors.New("serial number does not match UZI number") } - template, err := uraCredential(did, signingCert.NotAfter, otherNameValues, subjectTypes, subject) + template, err := uraCredential(*issuer, signingCert.NotAfter, otherNameValues, subjectTypes, subject) if err != nil { return nil, err } @@ -251,7 +259,7 @@ func Issue(chain validCertificateChain, key privateKey, subject subjectDID, opti } if hdrs.KeyID() == "" { - err := hdrs.Set("kid", did+"#0") + err := hdrs.Set("kid", issuer.String()+"#0") if err != nil { return "", err } @@ -274,7 +282,7 @@ func Issue(chain validCertificateChain, key privateKey, subject subjectDID, opti return "", err } - sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS512, rsa.PrivateKey(*key), jws.WithProtectedHeaders(hdrs))) + sign, err := jwt.Sign(token, jwt.WithKey(jwa.PS256, rsa.PrivateKey(*key), jws.WithProtectedHeaders(hdrs))) return string(sign), err }) } @@ -397,7 +405,7 @@ func convertHeaders(headers map[string]interface{}) (jws.Headers, error) { } // uraCredential builds a VerifiableCredential for a given URA and UZI number, including the subject's DID. -func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID subjectDID) (*vc.VerifiableCredential, error) { +func uraCredential(issuerDID did.DID, 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, @@ -410,17 +418,12 @@ func uraCredential(issuer string, expirationDate time.Time, otherNameValues []*x subject[string(subjectType.Type)] = subjectType.Value } - issuerDID, err := did.ParseDID(issuer) - if err != nil { - return nil, fmt.Errorf("failed to parse issuer DID '%s': %w", issuer, err) - } - id := did.DIDURL{ - DID: *issuerDID, + DID: issuerDID, Fragment: uuid.NewString(), }.URI() return &vc.VerifiableCredential{ - Issuer: ssi.MustParseURI(issuer), + Issuer: issuerDID.URI(), Context: []ssi.URI{ssi.MustParseURI("https://www.w3.org/2018/credentials/v1")}, Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential"), ssi.MustParseURI(CredentialType)}, ID: &id, diff --git a/uzi_vc_issuer/ura_issuer_test.go b/uzi_vc_issuer/ura_issuer_test.go index 3070042..eca1dbc 100644 --- a/uzi_vc_issuer/ura_issuer_test.go +++ b/uzi_vc_issuer/ura_issuer_test.go @@ -187,7 +187,7 @@ func TestIssue(t *testing.T) { assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String()) assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential"))) assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential"))) - 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()) + assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::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", @@ -210,7 +210,7 @@ func TestIssue(t *testing.T) { vc, err := Issue(validChain, validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization)) - 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%20%26%20Co", vc.Issuer.String()) + assert.Equal(t, "did:x509:0:sha256:IzvPueXLRjJtLtIicMzV3icpiLQPemu8lBv6oRGjm-o::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:O:FauxCare%20%26%20Co", vc.Issuer.String()) }) } diff --git a/x509_cert/x509_cert.go b/x509_cert/x509_cert.go index 6b3578a..56ddc06 100644 --- a/x509_cert/x509_cert.go +++ b/x509_cert/x509_cert.go @@ -2,17 +2,14 @@ package x509_cert import ( "crypto/rsa" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" "crypto/x509" "encoding/asn1" "errors" "fmt" - "github.com/lestrrat-go/jwx/v2/cert" - "golang.org/x/crypto/sha3" "regexp" "strings" + + "github.com/lestrrat-go/jwx/v2/cert" ) // SubjectAlternativeNameType represents the ASN.1 Object Identifier for Subject Alternative Name. @@ -28,27 +25,6 @@ var ( // var RegexOtherNameValue = regexp.MustCompile(`2\.16\.528\.1\.1007.\d+\.\d+-\d+-\d+-S-(\d+)-00\.000-\d+`) var RegexOtherNameValue = regexp.MustCompile(`^[0-9.]+-\d+-(\d+)-S-(\d+)-00\.000-(\d+)$`) -// Hash computes the hash of the input data using the specified algorithm. -// Supported algorithms include "sha1", "sha256", "sha384", and "sha512". -// Returns the computed hash as a byte slice or an error if the algorithm is not supported. -func Hash(data []byte, alg string) ([]byte, error) { - switch alg { - case "sha1": - sum := sha1.Sum(data) - return sum[:], nil - case "sha256": - sum := sha256.Sum256(data) - return sum[:], nil - case "sha384": - sum := sha3.Sum384(data) - return sum[:], nil - case "sha512": - sum := sha512.Sum512(data) - return sum[:], nil - } - return nil, fmt.Errorf("unsupported hash algorithm: %s", alg) -} - // ParseCertificates parses a slice of DER-encoded byte arrays into a slice of x509.Certificate. // It returns an error if any of the certificates cannot be parsed. func ParseCertificates(derChain [][]byte) ([]*x509.Certificate, error) { diff --git a/x509_cert/x509_cert_test.go b/x509_cert/x509_cert_test.go index 4f24a19..7893b41 100644 --- a/x509_cert/x509_cert_test.go +++ b/x509_cert/x509_cert_test.go @@ -1,78 +1,14 @@ package x509_cert import ( - "bytes" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" "crypto/x509" "encoding/pem" - "fmt" - "github.com/stretchr/testify/assert" - "golang.org/x/crypto/sha3" "strings" "testing" -) -func TestHash(t *testing.T) { - sha1sum := sha1.Sum([]byte("test")) - sha256sum := sha256.Sum256([]byte("test")) - sha384sum := sha3.Sum384([]byte("test")) - sha512sum := sha512.Sum512([]byte("test")) - testCases := []struct { - name string - data []byte - alg string - hash []byte - error error - }{ - { - name: "SHA1", - data: []byte("test"), - alg: "sha1", - hash: sha1sum[:], - }, - { - name: "SHA256", - data: []byte("test"), - alg: "sha256", - hash: sha256sum[:], - }, - { - name: "SHA384", - data: []byte("test"), - alg: "sha384", - hash: sha384sum[:], - }, - { - name: "SHA512", - data: []byte("test"), - alg: "sha512", - hash: sha512sum[:], - }, - { - name: "Unsupported", - data: []byte("test"), - alg: "unsupported", - hash: nil, - error: fmt.Errorf("unsupported hash algorithm: %s", "unsupported"), - }, - } + "github.com/stretchr/testify/assert" +) - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - hash, err := Hash(tc.data, tc.alg) - if tc.error != nil { - if err.Error() != tc.error.Error() { - t.Errorf("unexpected error %v, want %v", err, tc.error) - } - } - if !bytes.Equal(hash, tc.hash) { - t.Errorf("unexpected hash %x, want %x", hash, tc.hash) - } - }) - } -} func TestParseChain(t *testing.T) { _, chainPem, _, _, _, err := BuildSelfSignedCertChain("2.16.528.1.1007.99.2110-1-900030787-S-90000380-00.000-11223344", "900030787") failError(t, err)