Skip to content

Commit

Permalink
Merge pull request #38 from nuts-foundation/change-hashalg
Browse files Browse the repository at this point in the history
Use sha256 instead of sha512
  • Loading branch information
gerardsn authored Dec 18, 2024
2 parents dcc0eed + b43a258 commit 637ccd7
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 147 deletions.
31 changes: 19 additions & 12 deletions did_x509/did_x509.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package did_x509

import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"errors"
Expand All @@ -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
Expand All @@ -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)...)
Expand Down
53 changes: 26 additions & 27 deletions did_x509/did_x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand All @@ -65,39 +65,39 @@ 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: "",
},
{
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: "",
},
Expand All @@ -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)
}
})
Expand All @@ -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}
Expand All @@ -155,31 +154,31 @@ 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: "",
},
{
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: "",
},
{
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: "",
},
{
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: "",
Expand All @@ -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)
}
})
Expand Down
31 changes: 17 additions & 14 deletions uzi_vc_issuer/ura_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
})
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions uzi_vc_issuer/ura_issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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())
})

}
Expand Down
28 changes: 2 additions & 26 deletions x509_cert/x509_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 637ccd7

Please sign in to comment.