Skip to content

Commit

Permalink
Refactor SAN handling, add permanentIdentifier to the SAN attributes (#6
Browse files Browse the repository at this point in the history
)

* Refactor SAN handling and parsing logic

Replaced the single `otherName` SAN type detection with a more flexible approach that supports multiple SAN types and policies. Updated the respective functions and tests to accommodate this change, ensuring backward compatibility and improved error handling.

* Add FindOtherNameValue function to x509_utils.go

This function searches for a matching policyType and sanTypeName in a slice of OtherNameValue structs. It returns the corresponding value if found, or an error if no match is found.

* Add support for permanent identifier in self-signed certificates

Updated `BuildSelfSignedCertChain` and `SigningCertTemplate` to accept and process a permanent identifier value. This includes modifications to test cases and utilities to validate the new permanent identifier within certificate chains.

* Refactor `uraCredential` to add the otherName values dynamically.

Removed the redundant serialNumber parameter from the `uraCredential` function to streamline its usage. Adjusted the function internals accordingly to handle the credentials more effectively, ensuring the subject's DID and other name values are managed in a consolidated manner.

* Add codeclimate config
  • Loading branch information
rolandgroen authored Nov 5, 2024
1 parent 4f4b996 commit 3a4d054
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 87 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pem
uzi-did-x509-issuer
c.out
59 changes: 39 additions & 20 deletions did_x509/did_x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,32 @@ 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 {
return "", err
}
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) {
Expand All @@ -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.
Expand Down
68 changes: 63 additions & 5 deletions did_x509/did_x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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: "",
},
}
Expand All @@ -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)
}
})
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func main() {
// 2.16.528.1.1007.99.2110-1-<UZI-nr>-S-<Abonnee-nr>-00.000-<AGB-code>
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)
Expand Down
4 changes: 2 additions & 2 deletions pem/pem_reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
35 changes: 19 additions & 16 deletions uzi_vc_issuer/ura_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
19 changes: 10 additions & 9 deletions uzi_vc_issuer/ura_issuer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -51,15 +51,15 @@ 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)
failError(t, err)
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",
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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: "",
Expand Down
Loading

0 comments on commit 3a4d054

Please sign in to comment.