Skip to content

Commit df37a83

Browse files
authored
permanentIdentifier: only include if explicitly specified (#66)
1 parent 4f52c55 commit df37a83

File tree

5 files changed

+84
-50
lines changed

5 files changed

+84
-50
lines changed

credential_issuer/issuer.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,22 @@ var CredentialType = ssi.MustParseURI("X509Credential")
2929

3030
// issueOptions contains values for options for issuing a UZI VC.
3131
type issueOptions struct {
32-
includePermanentIdentifier bool
33-
subjectAttributes []x509_cert.SubjectTypeName
32+
subjectAttributes []x509_cert.SubjectTypeName
33+
sanAttributes []x509_cert.SanTypeName
3434
}
3535

3636
// Option is an interface for a function in the options pattern.
3737
type Option = func(*issueOptions)
3838

3939
var defaultIssueOptions = &issueOptions{
40-
includePermanentIdentifier: false,
41-
subjectAttributes: []x509_cert.SubjectTypeName{},
40+
sanAttributes: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName},
41+
subjectAttributes: []x509_cert.SubjectTypeName{},
4242
}
4343

4444
func Issue(chain []*x509.Certificate, caFingerprintCert *x509.Certificate, key *rsa.PrivateKey, subject string, optionFns ...Option) (*vc.VerifiableCredential, error) {
45-
options := defaultIssueOptions
45+
options := *defaultIssueOptions
4646
for _, fn := range optionFns {
47-
fn(options)
47+
fn(&options)
4848
}
4949

5050
// Sanity check: make sure caFingerprintCert is in the chain
@@ -59,27 +59,22 @@ func Issue(chain []*x509.Certificate, caFingerprintCert *x509.Certificate, key *
5959
return nil, errors.New("caFingerprintCert is not in the chain")
6060
}
6161

62-
types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName}
63-
if options.includePermanentIdentifier {
64-
types = append(types, x509_cert.SanTypePermanentIdentifierValue)
65-
types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)
66-
}
67-
68-
issuer, err := did_x509.CreateDid(chain[0], caFingerprintCert, options.subjectAttributes, types...)
62+
issuer, err := did_x509.CreateDid(chain[0], caFingerprintCert, options.subjectAttributes, options.sanAttributes...)
6963
if err != nil {
7064
return nil, err
7165
}
7266
// signing cert is at the start of the chain
7367
signingCert := chain[0]
74-
otherNameValues, err := x509_cert.FindSanTypes(signingCert)
68+
sanValues, err := x509_cert.SelectSanTypes(signingCert, options.sanAttributes...)
7569
if err != nil {
7670
return nil, err
7771
}
72+
7873
subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, options.subjectAttributes...)
7974
if err != nil {
8075
return nil, err
8176
}
82-
template, err := buildCredential(*issuer, signingCert.NotAfter, otherNameValues, subjectTypes, subject)
77+
template, err := buildCredential(*issuer, signingCert.NotAfter, sanValues, subjectTypes, subject)
8378
if err != nil {
8479
return nil, err
8580
}
@@ -130,13 +125,20 @@ func Issue(chain []*x509.Certificate, caFingerprintCert *x509.Certificate, key *
130125
})
131126
}
132127

133-
// SubjectAttributes sets the subject attributes to include in the UZI VC.
128+
// SubjectAttributes sets the subject attributes to include in the DID and VC.
134129
func SubjectAttributes(attributes ...x509_cert.SubjectTypeName) Option {
135130
return func(o *issueOptions) {
136131
o.subjectAttributes = attributes
137132
}
138133
}
139134

135+
// SANAttributes sets whether to include the SAN permanent identifier in the DID and VC.
136+
func SANAttributes(attributes ...x509_cert.SanTypeName) Option {
137+
return func(o *issueOptions) {
138+
o.sanAttributes = attributes
139+
}
140+
}
141+
140142
// marshalChain converts a slice of x509.Certificate instances to a cert.Chain, encoding each certificate as PEM.
141143
// It returns the PEM-encoded cert.Chain and an error if the encoding or header fixation fails.
142144
func marshalChain(certificates ...*x509.Certificate) (*cert.Chain, error) {

credential_issuer/issuer_test.go

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import (
55
"crypto/x509"
66
"crypto/x509/pkix"
77
"github.com/lestrrat-go/jwx/v2/jws"
8+
ssi "github.com/nuts-foundation/go-did"
89
"github.com/nuts-foundation/go-didx509-toolkit/internal"
910
"testing"
1011

11-
ssi "github.com/nuts-foundation/go-did"
1212
"github.com/nuts-foundation/go-didx509-toolkit/x509_cert"
1313
"github.com/stretchr/testify/assert"
1414
"github.com/stretchr/testify/require"
@@ -79,38 +79,67 @@ func TestIssue(t *testing.T) {
7979
validKey, err := internal.ParseRSAPrivateKeyFromPEM([]byte(internal.TestSigningKey))
8080
require.NoError(t, err, "failed to parse signing key")
8181
t.Run("ok - happy path", func(t *testing.T) {
82-
validChain, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain))
83-
require.NoError(t, err, "failed to parse chain")
82+
t.Run("include all", func(t *testing.T) {
83+
validChain, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain))
84+
require.NoError(t, err, "failed to parse chain")
85+
86+
vc, err := Issue(validChain, validChain[3], validKey, "did:example:123",
87+
SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization, x509_cert.SubjectTypeLocality),
88+
SANAttributes(x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierAssigner, x509_cert.SanTypePermanentIdentifierValue),
89+
)
90+
91+
require.NoError(t, err, "failed to issue verifiable credential")
92+
require.NotNil(t, vc, "verifiable credential is nil")
93+
94+
assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String())
95+
assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential")))
96+
assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential")))
97+
assert.Equal(t, "did:x509:0:sha256:DwXSf2_jaUod7cezXBGJBM4AaaoA8DI9j7aPMDTI-mQ::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333:permanentIdentifier.assigner:2.16.528.1.1007.3.3:permanentIdentifier.value:2222222::subject:L:Testland:O:Faux%20Care", vc.Issuer.String())
98+
99+
expectedCredentialSubject := []interface{}{map[string]interface{}{
100+
"id": "did:example:123",
101+
"subject": map[string]interface{}{
102+
"O": "Faux Care",
103+
"L": "Testland",
104+
},
105+
"san": map[string]interface{}{
106+
"otherName": "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333",
107+
"permanentIdentifier.assigner": "2.16.528.1.1007.3.3",
108+
"permanentIdentifier.value": "2222222",
109+
},
110+
}}
111+
112+
assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject)
113+
assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate, "expiration date of VC must match signing certificate")
114+
parsedJWT, err := jws.Parse([]byte(vc.Raw()))
115+
require.NoError(t, err)
116+
assert.Equal(t, "v4nyg4rKy6MBIxnutabaUwXCxYY", parsedJWT.Signatures()[0].ProtectedHeaders().X509CertThumbprint())
117+
assert.Equal(t, "XC-vUEDhKsMrtpwtYEQty5PgSj4ZphDLNDG_Rg9hQDk", parsedJWT.Signatures()[0].ProtectedHeaders().X509CertThumbprintS256())
118+
})
119+
t.Run("only include san/otherName", func(t *testing.T) {
120+
validChain, err := internal.ParseCertificatesFromPEM([]byte(internal.TestCertificateChain))
121+
require.NoError(t, err, "failed to parse chain")
84122

85-
vc, err := Issue(validChain, validChain[3], validKey, "did:example:123", SubjectAttributes(x509_cert.SubjectTypeCountry, x509_cert.SubjectTypeOrganization, x509_cert.SubjectTypeLocality))
123+
vc, err := Issue(validChain, validChain[3], validKey, "did:example:123")
86124

87-
require.NoError(t, err, "failed to issue verifiable credential")
88-
require.NotNil(t, vc, "verifiable credential is nil")
125+
require.NoError(t, err, "failed to issue verifiable credential")
126+
require.NotNil(t, vc, "verifiable credential is nil")
89127

90-
assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String())
91-
assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential")))
92-
assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential")))
93-
assert.Equal(t, "did:x509:0:sha256:DwXSf2_jaUod7cezXBGJBM4AaaoA8DI9j7aPMDTI-mQ::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333::subject:L:Testland:O:Faux%20Care", vc.Issuer.String())
128+
assert.Equal(t, "https://www.w3.org/2018/credentials/v1", vc.Context[0].String())
129+
assert.True(t, vc.IsType(ssi.MustParseURI("VerifiableCredential")))
130+
assert.True(t, vc.IsType(ssi.MustParseURI("X509Credential")))
131+
assert.Equal(t, "did:x509:0:sha256:DwXSf2_jaUod7cezXBGJBM4AaaoA8DI9j7aPMDTI-mQ::san:otherName:2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333", vc.Issuer.String())
94132

95-
expectedCredentialSubject := []interface{}{map[string]interface{}{
96-
"id": "did:example:123",
97-
"subject": map[string]interface{}{
98-
"O": "Faux Care",
99-
"L": "Testland",
100-
},
101-
"san": map[string]interface{}{
102-
"otherName": "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333",
103-
"permanentIdentifier.assigner": "2.16.528.1.1007.3.3",
104-
"permanentIdentifier.value": "2222222",
105-
},
106-
}}
133+
expectedCredentialSubject := []interface{}{map[string]interface{}{
134+
"id": "did:example:123",
135+
"san": map[string]interface{}{
136+
"otherName": "2.16.528.1.1007.99.2110-1-1111111-S-2222222-00.000-333333",
137+
},
138+
}}
107139

108-
assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject)
109-
assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate, "expiration date of VC must match signing certificate")
110-
parsedJWT, err := jws.Parse([]byte(vc.Raw()))
111-
require.NoError(t, err)
112-
assert.Equal(t, "v4nyg4rKy6MBIxnutabaUwXCxYY", parsedJWT.Signatures()[0].ProtectedHeaders().X509CertThumbprint())
113-
assert.Equal(t, "XC-vUEDhKsMrtpwtYEQty5PgSj4ZphDLNDG_Rg9hQDk", parsedJWT.Signatures()[0].ProtectedHeaders().X509CertThumbprintS256())
140+
assert.Equal(t, expectedCredentialSubject, vc.CredentialSubject)
141+
assert.Equal(t, validChain[0].NotAfter, *vc.ExpirationDate)
142+
})
114143
})
115144

116145
t.Run("ok - correct escaping of special characters", func(t *testing.T) {

did_x509/did_x509.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ func FormatDid(issuerCert *x509.Certificate, policy ...string) (*did.DID, error)
4949
// It extracts the Unique Registration Address (URA) from the chain, creates a policy with it, and formats the DID.
5050
// Returns the generated DID or an error if any step fails.
5151
func CreateDid(signingCert, caCert *x509.Certificate, includedSubjectTypes []x509_cert.SubjectTypeName, includedSanTypes ...x509_cert.SanTypeName) (*did.DID, error) {
52-
otherNames, err := x509_cert.SelectSanTypes(signingCert, includedSanTypes...)
52+
alternativeNames, err := x509_cert.SelectSanTypes(signingCert, includedSanTypes...)
5353
if err != nil {
5454
return nil, err
5555
}
56-
policies, err := formatPolicies(otherNames)
56+
policies, err := formatPolicies(alternativeNames)
5757
if err != nil {
5858
return nil, err
5959
}

main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type VC struct {
1919
CAFingerprintDN string `arg:"" short:"c" name:"ca_fingerprint_dn" help:"The full subject DN (distinguished name) of the CA certificate to be used as ca-fingerprint in the X.509 DID. The certificate must be present in the chain specified by certificate_file."`
2020
SubjectDID string `arg:"" name:"subject_did" help:"The subject DID of the Verifiable Credential."`
2121
SubjectAttributes []x509_cert.SubjectTypeName `short:"s" name:"subject_attr" help:"List of X.509 subject attributes to include in the Verifiable Credential." default:"O,L"`
22-
IncludePermanent bool `short:"p" help:"Include the permanent identifier in the did:x509."`
22+
SANAttributes []string `short:"a" help:"List of SAN attributes to include in the Verifiable Credential (options: 'otherName', 'permanentIdentifier.assigner', 'permanentIdentifier.value')." default:"otherName"`
2323
}
2424

2525
var _ error = InvalidCAFingerprintDNError{}
@@ -124,7 +124,10 @@ func issueVc(vc VC) (string, error) {
124124
return "", err
125125
}
126126

127-
credential, err := credential_issuer.Issue(chain, caFingerprintCert, key, vc.SubjectDID, credential_issuer.SubjectAttributes(vc.SubjectAttributes...))
127+
credential, err := credential_issuer.Issue(chain, caFingerprintCert, key, vc.SubjectDID,
128+
credential_issuer.SubjectAttributes(vc.SubjectAttributes...),
129+
credential_issuer.SANAttributes(vc.SANAttributes...),
130+
)
128131

129132
if err != nil {
130133
return "", err

x509_cert/x509_utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,14 @@ func FindSanTypes(certificate *x509.Certificate) ([]*PolicyValue, error) {
139139
return rv, nil
140140
}
141141

142-
func SelectSanTypes(certificate *x509.Certificate, subjectAttributes ...SanTypeName) ([]*PolicyValue, error) {
142+
func SelectSanTypes(certificate *x509.Certificate, sanTypes ...SanTypeName) ([]*PolicyValue, error) {
143143
subjectTypes, err := FindSanTypes(certificate)
144144
if err != nil {
145145
return nil, err
146146
}
147147
var selectedSubjectTypes []*PolicyValue
148148
for _, subjectType := range subjectTypes {
149-
if slices.Contains(subjectAttributes, subjectType.Type) {
149+
if slices.Contains(sanTypes, subjectType.Type) {
150150
selectedSubjectTypes = append(selectedSubjectTypes, subjectType)
151151
}
152152
}

0 commit comments

Comments
 (0)