Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor SAN handling, add permanentIdentifier to the SAN attributes #6

Merged
merged 11 commits into from
Nov 5, 2024
Merged
26 changes: 26 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -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
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
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
}
Loading
Loading