Skip to content

Commit

Permalink
Enhance verifiable credential issuance with new subject attributes
Browse files Browse the repository at this point in the history
Updated the verifiable credential issuance and validation functions to support additional subject attributes. This enhancement includes modifications to the Issue, BuildUraVerifiableCredential, and related methods to handle these new attributes. Added corresponding test cases and necessary utility functions for attribute selection and validation.
  • Loading branch information
rolandgroen committed Nov 12, 2024
1 parent 9a22269 commit d470876
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 96 deletions.
49 changes: 32 additions & 17 deletions did_x509/did_x509.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import (
"fmt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/uzi-did-x509-issuer/x509_cert"
"net/url"
"regexp"
"slices"
"strings"
)

type X509Did struct {
Version string
RootCertificateHash string
RootCertificateHashAlg string
Policies []*x509_cert.OtherNameValue
Policies []*x509_cert.GenericNameValue
}

// FormatDid constructs a decentralized identifier (DID) from a certificate chain and an optional policy.
Expand All @@ -35,18 +35,17 @@ func FormatDid(caCert *x509.Certificate, policy ...string) (string, error) {
// 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, types ...x509_cert.SanTypeName) (string, error) {
otherNames, err := x509_cert.FindSanTypes(signingCert)
func CreateDid(signingCert, caCert *x509.Certificate, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (string, error) {
otherNames, err := x509_cert.SelectSanTypes(signingCert, types...)
if err != nil {
return "", err
}
var selectedOtherNames []*x509_cert.OtherNameValue
for _, otherName := range otherNames {
if slices.Contains(types, otherName.Type) {
selectedOtherNames = append(selectedOtherNames, otherName)
}
}
policies := CreatePolicies(selectedOtherNames)
policies := CreateOtherNamePolicies(otherNames)

subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...)

policies = append(policies, CreateSubjectPolicies(subjectTypes)...)

formattedDid, err := FormatDid(caCert, policies...)
return formattedDid, err
}
Expand Down Expand Up @@ -78,22 +77,38 @@ func ParseDid(didString string) (*X509Did, error) {
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{
value, err := url.PathUnescape(submatch[3])
if err != nil {
return nil, err
}
x509Did.Policies = append(x509Did.Policies, &x509_cert.GenericNameValue{
PolicyType: x509_cert.PolicyType(submatch[1]),
Type: x509_cert.SanTypeName(submatch[2]),
Value: submatch[3],
Type: submatch[2],
Value: value,
})
}

return &x509Did, nil
}

// CreatePolicies constructs a policy string using the provided URA, fixed string "san", and "permanentIdentifier".
// CreateOtherNamePolicies 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 CreatePolicies(otherNames []*x509_cert.OtherNameValue) []string {
func CreateOtherNamePolicies(otherNames []*x509_cert.OtherNameValue) []string {
var policies []string
for _, otherName := range otherNames {
fragments := []string{string(otherName.PolicyType), string(otherName.Type), otherName.Value}
value := url.PathEscape(otherName.Value)
fragments := []string{string(otherName.PolicyType), string(otherName.Type), value}
policy := strings.Join(fragments, ":")
policies = append(policies, policy)
}
return policies
}

func CreateSubjectPolicies(subjectValues []*x509_cert.SubjectValue) []string {
var policies []string
for _, subjectValue := range subjectValues {
value := url.PathEscape(subjectValue.Value)
fragments := []string{string(subjectValue.PolicyType), string(subjectValue.Type), value}
policy := strings.Join(fragments, ":")
policies = append(policies, policy)
}
Expand Down
133 changes: 81 additions & 52 deletions did_x509/did_x509_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,57 +31,58 @@ func TestDefaultDidCreator_CreateDidSingle(t *testing.T) {
types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue, x509_cert.SanTypePermanentIdentifierAssigner}

tests := []struct {
name string
fields fields
args args
want string
errMsg string
types []x509_cert.SanTypeName
name string
fields fields
args args
want string
errMsg string
sanTypes []x509_cert.SanTypeName
subjectTypes []x509_cert.SubjectTypeName
}{
{
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"}, ":"),
types: 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", "", "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"}, ":"),
types: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue},
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"}, ":"),
sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue},
errMsg: "",
},
{
name: "Happy path",
fields: fields{},
args: args{chain: chain},
want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":"),
types: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName},
errMsg: "",
name: "Happy path",
fields: fields{},
args: args{chain: chain},
want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "otherName", "A_BIG_STRING"}, ":"),
sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypeOtherName},
errMsg: "",
},
{
name: "Happy path",
fields: fields{},
args: args{chain: chain},
want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING"}, ":"),
types: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierValue},
errMsg: "",
name: "Happy path",
fields: fields{},
args: args{chain: chain},
want: strings.Join([]string{"did", "x509", "0", alg, rootHashString, "", "san", "permanentIdentifier.value", "A_PERMANENT_STRING"}, ":"),
sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierValue},
errMsg: "",
},
{
name: "Happy path",
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"}, ":"),
types: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierAssigner},
errMsg: "",
name: "Happy path",
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"}, ":"),
sanTypes: []x509_cert.SanTypeName{x509_cert.SanTypePermanentIdentifierAssigner},
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], tt.types...)
got, err := CreateDid(tt.args.chain[0], tt.args.chain[len(tt.args.chain)-1], tt.subjectTypes, tt.sanTypes...)
wantErr := tt.errMsg != ""
if (err != nil) != wantErr {
t.Errorf("DefaultDidProcessor.CreateDid() error = %v, errMsg %v", err, tt.errMsg)
Expand Down Expand Up @@ -115,28 +116,56 @@ func TestDefaultDidCreator_CreateDidDouble(t *testing.T) {
t.Fatal(err)
}
rootHashString := base64.RawURLEncoding.EncodeToString(hash)
types := []x509_cert.SanTypeName{x509_cert.SanTypeOtherName, x509_cert.SanTypePermanentIdentifierValue, x509_cert.SanTypePermanentIdentifierAssigner}
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}

tests := []struct {
name string
fields fields
args args
want string
errMsg string
types []x509_cert.SanTypeName
name string
fields fields
args args
want string
errMsg string
sanTypes []x509_cert.SanTypeName
subjectTypes []x509_cert.SubjectTypeName
}{
{
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"}, ":"),
types: types,
errMsg: "",
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"}, ":"),
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"}, ":"),
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"}, ":"),
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"}, ":"),
sanTypes: sanTypeNamesShort,
subjectTypes: subjectTypeNamesShort,
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], tt.types...)
got, err := CreateDid(tt.args.chain[0], tt.args.chain[len(tt.args.chain)-1], tt.subjectTypes, tt.sanTypes...)
wantErr := tt.errMsg != ""
if (err != nil) != wantErr {
t.Errorf("DefaultDidProcessor.CreateDid() error = %v, errMsg %v", err, tt.errMsg)
Expand All @@ -157,7 +186,7 @@ func TestDefaultDidCreator_CreateDidDouble(t *testing.T) {
// 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{
policies := []*x509_cert.GenericNameValue{
{
PolicyType: "san",
Type: "otherName",
Expand Down
13 changes: 7 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (
)

type VC struct {
CertificateFile string `arg:"" name:"certificate_file" help:"Certificate PEM file. If the file contains a chain, the chain will be used for signing." type:"existingfile"`
SigningKey string `arg:"" name:"signing_key" help:"PEM key for signing." type:"existingfile"`
SubjectDID string `arg:"" name:"subject_did" help:"The subject DID of the VC." type:"key"`
Test bool `short:"t" help:"Allow for certificates signed by the TEST UZI Root CA."`
IncludePermanent bool `short:"p" help:"Include the permanent identifier in the did:x509."`
CertificateFile string `arg:"" name:"certificate_file" help:"Certificate PEM file. If the file contains a chain, the chain will be used for signing." type:"existingfile"`
SigningKey string `arg:"" name:"signing_key" help:"PEM key for signing." type:"existingfile"`
SubjectDID string `arg:"" name:"subject_did" help:"The subject DID of the VC."`
SubjectAttributes []x509_cert.SubjectTypeName `short:"s" name:"subject_attr" help:"A list of Subject Attributes u in the VC." default:"O,L"`
Test bool `short:"t" help:"Allow for certificates signed by the TEST UZI Root CA."`
IncludePermanent bool `short:"p" help:"Include the permanent identifier in the did:x509."`
}

type TestCert struct {
Expand Down Expand Up @@ -122,5 +123,5 @@ func printLineAndFlush(jwt string) error {
}

func issueVc(vc VC) (string, error) {
return uzi_vc_issuer.Issue(vc.CertificateFile, vc.SigningKey, vc.SubjectDID, vc.Test, vc.IncludePermanent)
return uzi_vc_issuer.Issue(vc.CertificateFile, vc.SigningKey, vc.SubjectDID, vc.Test, vc.IncludePermanent, vc.SubjectAttributes)
}
24 changes: 16 additions & 8 deletions uzi_vc_issuer/ura_issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
)

// Issue generates a URA Verifiable Credential using provided certificate, signing key, subject DID, and subject name.
func Issue(certificateFile string, signingKeyFile string, subjectDID string, allowTestUraCa bool, includePermanentIdentifier bool) (string, error) {
func Issue(certificateFile string, signingKeyFile string, subjectDID string, allowTestUraCa bool, includePermanentIdentifier bool, subjectAttributes []x509_cert.SubjectTypeName) (string, error) {
pemBlocks, err := pem2.ParseFileOrPath(certificateFile, "CERTIFICATE")
if err != nil {
return "", err
Expand Down Expand Up @@ -72,7 +72,7 @@ func Issue(certificateFile string, signingKeyFile string, subjectDID string, all
types = append(types, x509_cert.SanTypePermanentIdentifierValue)
types = append(types, x509_cert.SanTypePermanentIdentifierAssigner)
}
credential, err := BuildUraVerifiableCredential(chain, privateKey, subjectDID, types...)
credential, err := BuildUraVerifiableCredential(chain, privateKey, subjectDID, subjectAttributes, types...)
if err != nil {
return "", err
}
Expand All @@ -86,14 +86,14 @@ func Issue(certificateFile string, signingKeyFile string, subjectDID string, all
}

// BuildUraVerifiableCredential constructs a verifiable credential with specified certificates, signing key, subject DID.
func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string, types ...x509_cert.SanTypeName) (*vc.VerifiableCredential, error) {
func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.PrivateKey, subjectDID string, subjectAttributes []x509_cert.SubjectTypeName, types ...x509_cert.SanTypeName) (*vc.VerifiableCredential, error) {
if len(chain) == 0 {
return nil, errors.New("empty certificate chain")
}
if signingKey == nil {
return nil, errors.New("signing key is nil")
}
did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], types...)
did, err := did_x509.CreateDid(chain[0], chain[len(chain)-1], subjectAttributes, types...)
if err != nil {
return nil, err
}
Expand All @@ -107,6 +107,10 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
if err != nil {
return nil, err
}
subjectTypes, err := x509_cert.SelectSubjectTypes(signingCert, subjectAttributes...)
if err != nil {
return nil, err
}
stringValue, err := x509_cert.FindOtherNameValue(otherNameValues, x509_cert.PolicyTypeSan, x509_cert.SanTypeOtherName)
uzi, _, _, err := x509_cert.ParseUraFromOtherNameValue(stringValue)
if err != nil {
Expand All @@ -115,7 +119,7 @@ func BuildUraVerifiableCredential(chain []*x509.Certificate, signingKey *rsa.Pri
if uzi != serialNumber {
return nil, errors.New("serial number does not match UZI number")
}
template, err := uraCredential(did, otherNameValues, subjectDID)
template, err := uraCredential(did, otherNameValues, subjectTypes, subjectDID)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -260,14 +264,18 @@ 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, otherNameValues []*x509_cert.OtherNameValue, subjectDID string) (*vc.VerifiableCredential, error) {
func uraCredential(issuer string, otherNameValues []*x509_cert.OtherNameValue, subjectTypes []*x509_cert.SubjectValue, subjectDID string) (*vc.VerifiableCredential, error) {
exp := time.Now().Add(time.Hour * 24 * 365 * 100)
iat := time.Now()
subject := map[x509_cert.SanTypeName]interface{}{
subject := map[string]interface{}{
"id": subjectDID,
}
for _, otherNameValue := range otherNameValues {
subject[otherNameValue.Type] = otherNameValue.Value
subject[string(otherNameValue.Type)] = otherNameValue.Value
}

for _, subjectType := range subjectTypes {
subject[string(subjectType.Type)] = subjectType.Value
}

return &vc.VerifiableCredential{
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 @@ -99,7 +99,7 @@ func TestBuildUraVerifiableCredential(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
certificates := clo.Clone(_certs).([]*x509.Certificate)
certificates, signingKey, subjectDID := tt.in(certificates)
_, err := BuildUraVerifiableCredential(certificates, signingKey, subjectDID)
_, err := BuildUraVerifiableCredential(certificates, signingKey, subjectDID, []x509_cert.SubjectTypeName{})
if err != nil {
if err.Error() != tt.errorText {
t.Errorf("BuildUraVerifiableCredential() error = '%v', wantErr '%v'", err.Error(), tt.errorText)
Expand Down Expand Up @@ -324,7 +324,7 @@ func TestIssue(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Issue(tt.certFile, tt.keyFile, tt.subjectDID, tt.allowTest, true)
result, err := Issue(tt.certFile, tt.keyFile, tt.subjectDID, tt.allowTest, true, make([]x509_cert.SubjectTypeName, 0))
if err != nil {
if err.Error() != tt.errorText {
t.Errorf("Issue() error = '%v', wantErr '%v'", err.Error(), tt.errorText)
Expand Down
Loading

0 comments on commit d470876

Please sign in to comment.