From d8e0117fbb56bbdc42dfdcb75c84c087fef07615 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 25 Jun 2024 15:44:26 +0200 Subject: [PATCH 1/3] go-did/go-vc version that follows the DID/VC specs more closely --- .gitignore | 28 ++++++++-- cmd/didcore.go | 59 ++++++++++++++++++++ cmd/main.go | 117 +++++++++++++++++++++++++++++++++++++++ cmd/vc.go | 44 +++++++++++++++ did/document.go | 79 +++++++++++++++++++++++++- did/generator.go | 5 ++ go.mod | 2 + go.sum | 4 ++ v1/ld/converters.go | 76 +++++++++++++++++++++++++ v1/ld/documentloader.go | 12 ++++ v1/ld/model.go | 48 ++++++++++++++++ v1/vc/codegen/main.go | 82 +++++++++++++++++++++++++++ v1/vc/model.gen.go | 72 ++++++++++++++++++++++++ v1/vc/test/example7.json | 25 +++++++++ v1/vc/vc.go | 42 ++++++++++++++ v1/vc/vc_test.go | 44 +++++++++++++++ 16 files changed, 732 insertions(+), 7 deletions(-) create mode 100644 cmd/didcore.go create mode 100644 cmd/main.go create mode 100644 cmd/vc.go create mode 100644 did/generator.go create mode 100644 v1/ld/converters.go create mode 100644 v1/ld/documentloader.go create mode 100644 v1/ld/model.go create mode 100644 v1/vc/codegen/main.go create mode 100644 v1/vc/model.gen.go create mode 100644 v1/vc/test/example7.json create mode 100644 v1/vc/vc.go create mode 100644 v1/vc/vc_test.go diff --git a/.gitignore b/.gitignore index 8e3dfa9..b358304 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,29 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib -# Editors +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# IntelliJ IDEA files .idea *.iml - -# MacOS .DS_Store -c.out \ No newline at end of file +*.bicepparam +!*empty.bicepparam diff --git a/cmd/didcore.go b/cmd/didcore.go new file mode 100644 index 0000000..eb5f6fa --- /dev/null +++ b/cmd/didcore.go @@ -0,0 +1,59 @@ +package main + +func didDocument() TypeDefinition { + return TypeDefinition{ + Name: "Document", + Fields: []FieldDefinition{ + { + Name: "Context", + JSONName: "@context", + GoType: "[]interface", + }, + { + Name: "ID", + JSONName: "id", + GoType: "DID", + }, + { + Name: "AlsoKnownAs", + JSONName: "alsoKnownAs", + GoType: "[]ssi.URI", + }, + { + Name: "VerificationMethod", + JSONName: "verificationMethod", + GoType: "VerificationMethods", + }, + { + Name: "Authentication", + JSONName: "authentication", + GoType: "VerificationRelationships", + }, + { + Name: "AssertionMethod", + JSONName: "assertionMethod", + GoType: "VerificationRelationships", + }, + { + Name: "KeyAgreement", + JSONName: "keyAgreement", + GoType: "VerificationRelationships", + }, + { + Name: "CapabilityInvocation", + JSONName: "capabilityInvocation", + GoType: "VerificationRelationships", + }, + { + Name: "CapabilityDelegation", + JSONName: "capabilityDelegation", + GoType: "VerificationRelationships", + }, + { + Name: "Service", + JSONName: "service", + GoType: "[]Service", + }, + }, + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..1df5852 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "os" +) + +func main() { + err := os.WriteFile("../v1/vc/model.gen.go", []byte(generate("vc", verifiableCredential())), 0644) + if err != nil { + panic(err) + } +} + +type TypeDefinition struct { + Name string + Fields []FieldDefinition +} + +type FieldDefinition struct { + Name string + JSONName string + IRI string + Required bool + DocLink string + GoType string +} + +func generate(pkg string, def TypeDefinition) string { + implType := "LD" + def.Name + buf := "" + buf += "package " + pkg + "\n\n" + buf += "\n" + buf += `import ( + "github.com/nuts-foundation/go-did/v1/ld" + "time" +)` + buf += "\n\n" + // Interface type + buf += "type " + def.Name + " interface {\n" + buf += "\tld.Object\n" + buf += "\tContext() []interface{}\n" + for _, field := range def.Fields { + buf += "\t// " + field.Name + " as defined by " + field.DocLink + "\n" + if field.Required { + buf += "\t" + field.Name + "() " + field.GoType + "\n" + } else { + buf += "\t" + field.Name + "() (bool, " + field.GoType + ")\n" + } + } + buf += "}\n" + buf += "\n" + // Implementation type + buf += "var _ " + def.Name + " = &" + implType + "{}\n" + buf += "\n" + buf += "type " + implType + " struct {\n" + buf += "\tld.Object\n" + buf += "\tcontext []interface{}\n" + buf += "}\n" + buf += "\n" + // Fixed Context field + buf += "func (o " + implType + ") Context() []interface{} {\n" + buf += "\treturn o.context\n" + buf += "}\n\n" + // Type-specific fields + for _, field := range def.Fields { + returnType := field.GoType + if !field.Required { + returnType = "(bool, " + field.GoType + ")" + } + buf += "func (o " + implType + ") " + field.Name + "() " + returnType + " {\n" + if field.Required { + buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" + buf += "\tif !ok {\n" + buf += "\t\treturn " + nilValue(field.GoType) + "\n" + buf += "\t}\n" + buf += "\treturn " + converterFunc(field.GoType) + "(obj)\n" + } else { + buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" + buf += "\tif !ok {\n" + buf += "\t\treturn false, " + converterFunc(field.GoType) + "(nil)\n" + buf += "\t}\n" + buf += "\treturn true, " + converterFunc(field.GoType) + "(obj)\n" + } + buf += "}\n\n" + } + return buf +} + +func nilValue(goType string) string { + switch goType { + case "ld.IDObject": + return "ld.IDObject{}" + case "time.Time": + return "time.Time{}" + default: + return "nil" + } +} + +func converterFunc(goType string) string { + switch goType { + case "ld.Object": + return "ld.ToObject" + case "[]ld.Object": + return "ld.ToObjects" + case "ld.IDObject": + return "ld.NewIDObject" + case "time.Time": + return "ld.ToTime" + case "[]string": + return "ld.ToStrings" + case "[]interface{}": + return "ld.ToInterfaces" + default: + return "MISSING_CONVERTER" + } +} diff --git a/cmd/vc.go b/cmd/vc.go new file mode 100644 index 0000000..d0a3c56 --- /dev/null +++ b/cmd/vc.go @@ -0,0 +1,44 @@ +package main + +func verifiableCredential() TypeDefinition { + return TypeDefinition{ + Name: "VerifiableCredential", + Fields: []FieldDefinition{ + { + Name: "Type", + GoType: "[]string", + Required: true, + IRI: "@type", + DocLink: "https://www.w3.org/TR/vc-data-model/#types", + }, + { + Name: "Issuer", + GoType: "ld.IDObject", + Required: true, + IRI: "https://www.w3.org/2018/credentials#issuer", + DocLink: "https://www.w3.org/TR/vc-data-model/#issuer", + }, + { + Name: "IssuanceDate", + GoType: "time.Time", + Required: true, + IRI: "https://www.w3.org/2018/credentials#issuanceDate", + DocLink: "https://www.w3.org/TR/vc-data-model/#issuance", + }, + { + Name: "ExpirationDate", + GoType: "time.Time", + Required: false, + IRI: "https://www.w3.org/2018/credentials#expirationDate", + DocLink: "https://www.w3.org/TR/vc-data-model/#expiration", + }, + { + Name: "CredentialSubject", + GoType: "[]ld.Object", + Required: true, + IRI: "https://www.w3.org/2018/credentials#credentialSubject", + DocLink: "https://www.w3.org/TR/vc-data-model/#credential-subject", + }, + }, + } +} diff --git a/did/document.go b/did/document.go index f78dd3d..2e0a56b 100644 --- a/did/document.go +++ b/did/document.go @@ -297,8 +297,9 @@ type VerificationMethod struct { Controller DID `json:"controller,omitempty"` PublicKeyMultibase string `json:"publicKeyMultibase,omitempty"` // PublicKeyBase58 is deprecated and should not be used anymore. Use PublicKeyMultibase or PublicKeyJwk instead. - PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` - PublicKeyJwk map[string]interface{} `json:"publicKeyJwk,omitempty"` + PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` + PublicKeyJwk map[string]interface{} `json:"publicKeyJwk,omitempty"` + additionalProperties map[string]interface{} } // NewVerificationMethod is a convenience method to easily create verificationMethods based on a set of given params. @@ -340,7 +341,7 @@ func NewVerificationMethod(id DIDURL, keyType ssi.KeyType, controller DID, key c } vm.PublicKeyJwk = jwkAsMap } - if keyType == ssi.ED25519VerificationKey2018 || keyType == ssi.ED25519VerificationKey2020 { + if keyType == ssi.ED25519VerificationKey2018 || keyType == ssi.ED25519VerificationKey2020 { ed25519Key, ok := key.(ed25519.PublicKey) if !ok { return nil, errors.New("wrong key type") @@ -355,6 +356,69 @@ func NewVerificationMethod(id DIDURL, keyType ssi.KeyType, controller DID, key c return vm, nil } +func (v VerificationMethod) Get(propertyName string) (interface{}, bool) { + switch propertyName { + case "id": + return v.ID, true + case "type": + return v.Type, true + case "controller": + return v.Controller, true + case "publicKeyMultibase": + return v.PublicKeyMultibase, true + case "publicKeyBase58": + return v.PublicKeyBase58, true + case "publicKeyJwk": + return v.PublicKeyJwk, true + } + result, ok := v.additionalProperties[propertyName] + return result, ok +} + +func (v *VerificationMethod) Set(propertyName string, value interface{}) error { + if v.additionalProperties == nil { + v.additionalProperties = make(map[string]interface{}) + } + switch propertyName { + case "id": + if id, ok := value.(DIDURL); ok { + v.ID = id + } else { + return errors.New("invalid type for id") + } + case "type": + if keyType, ok := value.(ssi.KeyType); ok { + v.Type = keyType + } else { + return errors.New("invalid type for type") + } + case "controller": + if controller, ok := value.(DID); ok { + v.Controller = controller + } else { + return errors.New("invalid type for controller") + } + case "publicKeyMultibase": + if publicKeyMultibase, ok := value.(string); ok { + v.PublicKeyMultibase = publicKeyMultibase + } + case "publicKeyBase58": + if publicKeyBase58, ok := value.(string); ok { + v.PublicKeyBase58 = publicKeyBase58 + } + case "publicKeyJwk": + if publicKeyJwk, ok := value.(map[string]interface{}); ok { + v.PublicKeyJwk = publicKeyJwk + } + default: + if v.additionalProperties == nil { + v.additionalProperties = make(map[string]interface{}) + } + v.additionalProperties[propertyName] = value + } + +} + // JWK returns the key described by the VerificationMethod as JSON Web Key. func (v VerificationMethod) JWK() (jwk.Key, error) { if v.PublicKeyJwk == nil { @@ -459,6 +523,15 @@ func parseKeyID(b []byte) (*DIDURL, error) { return ParseDIDURL(keyIDString) } +func (v VerificationMethod) MarshalJSON() ([]byte, error) { + type Alias VerificationMethod + data, err := json.Marshal(Alias(v)) + if err != nil { + return nil, err + } + +} + func (v *VerificationMethod) UnmarshalJSON(bytes []byte) error { // Use an alias since ID should conform to DID URL syntax, not DID syntax type alias struct { diff --git a/did/generator.go b/did/generator.go new file mode 100644 index 0000000..11b46a1 --- /dev/null +++ b/did/generator.go @@ -0,0 +1,5 @@ +package did + +func main() { + +} diff --git a/go.mod b/go.mod index 36f927b..9a364db 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,9 @@ require ( github.com/mr-tron/base58 v1.1.0 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect + github.com/piprate/json-gold v0.5.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/segmentio/asm v1.2.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index 1485727..f7b8dff 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,12 @@ github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ8 github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM= github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/piprate/json-gold v0.5.0 h1:RmGh1PYboCFcchVFuh2pbSWAZy4XJaqTMU4KQYsApbM= +github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= +github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= diff --git a/v1/ld/converters.go b/v1/ld/converters.go new file mode 100644 index 0000000..d8911c3 --- /dev/null +++ b/v1/ld/converters.go @@ -0,0 +1,76 @@ +package ld + +import ( + "time" +) + +func ToStrings(value interface{}) []string { + var results []string + values, ok := value.([]interface{}) + if ok { + for _, raw := range values { + val, ok := raw.(string) + if ok { + results = append(results, val) + } + } + } + return results +} + +func ToTime(obj interface{}) time.Time { + value, ok := getValue(obj).(string) + if !ok { + return time.Time{} + } + result, _ := time.Parse(time.RFC3339, value) + return result +} + +func NewIDObject(obj interface{}) IDObject { + return IDObject{ + map[string]interface{}{ + "@id": obj, + }, + } +} + +func getValue(input interface{}) interface{} { + asSlice, ok := input.([]interface{}) + if !ok || len(asSlice) == 0 { + return nil + } + asMap, ok := asSlice[0].(map[string]interface{}) + if !ok { + return nil + } + return asMap["@value"] +} + +func ToInterfaces(input interface{}) []interface{} { + asSlice, ok := input.([]interface{}) + if !ok || len(asSlice) == 0 { + return nil + } + return asSlice +} + +func ToObject(input interface{}) Object { + asMap, ok := input.(map[string]interface{}) + if !ok { + return BaseObject{} + } + return BaseObject(asMap) +} + +func ToObjects(obj interface{}) []Object { + asSlice, ok := obj.([]interface{}) + if !ok { + return nil + } + var results []Object + for _, raw := range asSlice { + results = append(results, ToObject(raw)) + } + return results +} diff --git a/v1/ld/documentloader.go b/v1/ld/documentloader.go new file mode 100644 index 0000000..821833d --- /dev/null +++ b/v1/ld/documentloader.go @@ -0,0 +1,12 @@ +package ld + +import ( + "github.com/piprate/json-gold/ld" + "net/http" +) + +type DocumentLoader = ld.DocumentLoader + +func Loader() ld.DocumentLoader { + return ld.NewDefaultDocumentLoader(http.DefaultClient) +} diff --git a/v1/ld/model.go b/v1/ld/model.go new file mode 100644 index 0000000..8f80810 --- /dev/null +++ b/v1/ld/model.go @@ -0,0 +1,48 @@ +package ld + +import "net/url" + +type Object interface { + Set(string, interface{}) error + Get(string) (bool, interface{}) + ID() (bool, *url.URL) +} + +var _ Object = &BaseObject{} + +type BaseObject map[string]interface{} + +func (o BaseObject) ID() (bool, *url.URL) { + ok, id := o.Get("@id") + if !ok { + return false, &url.URL{} + } + result, err := url.Parse(id.(string)) + if err != nil { + return false, &url.URL{} + } + return true, result +} + +func (o BaseObject) Set(s string, i interface{}) error { + //TODO implement me + panic("implement me") +} + +func (o BaseObject) Get(s string) (bool, interface{}) { + v, ok := o[s] + return ok, v +} + +// IDObject is an Object which is guaranteed to have an ID property. +type IDObject struct { + BaseObject +} + +func (U IDObject) ID() *url.URL { + ok, u := U.BaseObject.ID() + if !ok { + return &url.URL{} + } + return u +} diff --git a/v1/vc/codegen/main.go b/v1/vc/codegen/main.go new file mode 100644 index 0000000..7ad1121 --- /dev/null +++ b/v1/vc/codegen/main.go @@ -0,0 +1,82 @@ +package main + +func main() { + didDocumentDefintion := TypeDefinition{ + Name: "Document", + Fields: []FieldDefinition{ + { + Name: "Context", + JSONName: "@context", + GoType: "[]interface", + }, + { + Name: "ID", + JSONName: "id", + GoType: "DID", + }, + { + Name: "AlsoKnownAs", + JSONName: "alsoKnownAs", + GoType: "[]ssi.URI", + }, + { + Name: "VerificationMethod", + JSONName: "verificationMethod", + GoType: "VerificationMethods", + }, + { + Name: "Authentication", + JSONName: "authentication", + GoType: "VerificationRelationships", + }, + { + Name: "AssertionMethod", + JSONName: "assertionMethod", + GoType: "VerificationRelationships", + }, + { + Name: "KeyAgreement", + JSONName: "keyAgreement", + GoType: "VerificationRelationships", + }, + { + Name: "CapabilityInvocation", + JSONName: "capabilityInvocation", + GoType: "VerificationRelationships", + }, + { + Name: "CapabilityDelegation", + JSONName: "capabilityDelegation", + GoType: "VerificationRelationships", + }, + { + Name: "Service", + JSONName: "service", + GoType: "[]Service", + }, + }, + } +} + +type TypeDefinition struct { + Name string + Fields []FieldDefinition +} + +type FieldDefinition struct { + Name string + JSONName string + GoType string +} + +func generate(typeDef TypeDefinition) string { + buf := "" + buf += "package did\n\n" + buf += "type " + typeDef.Name + " struct {\n" + buf += "\t properties map[string]interface{}" + buf += "}\n" + buf += "\n" + buf += "func (d " + typeDef.Name + ") Get(key string) interface{} {\n" + buf += "\t return d.properties[key]\n" + buf += "}\n" +} diff --git a/v1/vc/model.gen.go b/v1/vc/model.gen.go new file mode 100644 index 0000000..ab34f1f --- /dev/null +++ b/v1/vc/model.gen.go @@ -0,0 +1,72 @@ +package vc + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + "time" +) + +type VerifiableCredential interface { + ld.Object + Context() []interface{} + // Type as defined by https://www.w3.org/TR/vc-data-model/#types + Type() []string + // Issuer as defined by https://www.w3.org/TR/vc-data-model/#issuer + Issuer() ld.IDObject + // IssuanceDate as defined by https://www.w3.org/TR/vc-data-model/#issuance + IssuanceDate() time.Time + // ExpirationDate as defined by https://www.w3.org/TR/vc-data-model/#expiration + ExpirationDate() (bool, time.Time) + // CredentialSubject as defined by https://www.w3.org/TR/vc-data-model/#credential-subject + CredentialSubject() []ld.Object +} + +var _ VerifiableCredential = &LDVerifiableCredential{} + +type LDVerifiableCredential struct { + ld.Object + context []interface{} +} + +func (o LDVerifiableCredential) Context() []interface{} { + return o.context +} + +func (o LDVerifiableCredential) Type() []string { + ok, obj := o.Get("@type") + if !ok { + return nil + } + return ld.ToStrings(obj) +} + +func (o LDVerifiableCredential) Issuer() ld.IDObject { + ok, obj := o.Get("https://www.w3.org/2018/credentials#issuer") + if !ok { + return ld.IDObject{} + } + return ld.NewIDObject(obj) +} + +func (o LDVerifiableCredential) IssuanceDate() time.Time { + ok, obj := o.Get("https://www.w3.org/2018/credentials#issuanceDate") + if !ok { + return time.Time{} + } + return ld.ToTime(obj) +} + +func (o LDVerifiableCredential) ExpirationDate() (bool, time.Time) { + ok, obj := o.Get("https://www.w3.org/2018/credentials#expirationDate") + if !ok { + return false, ld.ToTime(nil) + } + return true, ld.ToTime(obj) +} + +func (o LDVerifiableCredential) CredentialSubject() []ld.Object { + ok, obj := o.Get("https://www.w3.org/2018/credentials#credentialSubject") + if !ok { + return nil + } + return ld.ToObjects(obj) +} diff --git a/v1/vc/test/example7.json b/v1/vc/test/example7.json new file mode 100644 index 0000000..bb74395 --- /dev/null +++ b/v1/vc/test/example7.json @@ -0,0 +1,25 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "RelationshipCredential" + ], + "issuer": "https://example.com/issuer/123", + "issuanceDate": "2010-01-01T00:00:00Z", + "credentialSubject": [ + { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Jayden Doe", + "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" + }, + { + "id": "did:example:c276e12ec21ebfeb1f712ebc6f1", + "name": "Morgan Doe", + "spouse": "did:example:ebfeb1f712ebc6f1c276e12ec21" + } + ] +} \ No newline at end of file diff --git a/v1/vc/vc.go b/v1/vc/vc.go new file mode 100644 index 0000000..5838da4 --- /dev/null +++ b/v1/vc/vc.go @@ -0,0 +1,42 @@ +package vc + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/nuts-foundation/go-did/v1/ld" + libld "github.com/piprate/json-gold/ld" +) + +func Parse(raw string, documentLoader ld.DocumentLoader) (VerifiableCredential, error) { + document, err := libld.DocumentFromReader(bytes.NewReader([]byte(raw))) + if err != nil { + return nil, fmt.Errorf("jsonld read: %w", err) + } + + processor := libld.NewJsonLdProcessor() + options := libld.NewJsonLdOptions("") + options.DocumentLoader = documentLoader + options.SafeMode = true + + expanded, err := processor.Expand(document, options) + if err != nil { + return nil, fmt.Errorf("jsonld expand: %w", err) + } + if len(expanded) != 1 { + return nil, fmt.Errorf("jsonld expand: expected 1 document") + } + var result LDVerifiableCredential + result.Object = ld.ToObject(expanded[0]).(ld.BaseObject) + result.context = unmarshalContext([]byte(raw)) + return &result, nil +} + +func unmarshalContext(input []byte) []interface{} { + type context struct { + Context []interface{} `json:"@context"` + } + var c context + _ = json.Unmarshal(input, &c) + return c.Context +} diff --git a/v1/vc/vc_test.go b/v1/vc/vc_test.go new file mode 100644 index 0000000..a7decdf --- /dev/null +++ b/v1/vc/vc_test.go @@ -0,0 +1,44 @@ +package vc + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestParse(t *testing.T) { + data, err := os.ReadFile("test/example7.json") + require.NoError(t, err) + + actual, err := Parse(string(data), ld.Loader()) + + require.NoError(t, err) + require.NotNil(t, actual) + + // ID + ok, id := actual.ID() + require.True(t, ok) + require.Equal(t, "http://example.edu/credentials/3732", id.ID().String()) + // IssuanceDate + require.Equal(t, "2010-01-01 00:00:00 +0000 UTC", actual.IssuanceDate().String()) + // Type + require.Len(t, actual.Type(), 2) + require.Contains(t, actual.Type(), "https://www.w3.org/2018/credentials#VerifiableCredential") + require.Contains(t, actual.Type(), "https://example.org/examples#RelationshipCredential") + // Context + require.Len(t, actual.Context(), 2) + require.Equal(t, "https://www.w3.org/2018/credentials/v1", actual.Context()[0]) + require.Equal(t, "https://www.w3.org/2018/credentials/examples/v1", actual.Context()[1]) + // CredentialSubject + subjects := actual.CredentialSubject() + require.Len(t, subjects, 2) + { + subject := subjects[0] + ok, id := subject.ID() + require.True(t, ok) + require.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", id.String()) + subject.Get() + } + +} From e397900141ae62ce50b826abbfa1209970833309 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 25 Jun 2024 15:45:01 +0200 Subject: [PATCH 2/3] revert --- .gitignore | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index b358304..8e3dfa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,9 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -# Go workspace file -go.work - -# IntelliJ IDEA files +# Editors .idea *.iml + +# MacOS .DS_Store -*.bicepparam -!*empty.bicepparam +c.out \ No newline at end of file From a651a81b9e6a7837df586e13ae1e1f178a2fea86 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Fri, 28 Jun 2024 21:34:53 +0200 Subject: [PATCH 3/3] wip --- cmd/didcore.go | 16 +- cmd/jsonld.go | 50 ++++++ cmd/jwt.go | 11 ++ cmd/main.go | 151 +++++++++--------- cmd/vc.go | 75 ++++++++- v1/did/model.gen.go | 31 ++++ v1/ld/converters.go | 8 - v1/ld/model.go | 23 ++- v1/vc/credential_subject.gen.go | 35 ++++ v1/vc/issuer.gen.go | 35 ++++ v1/vc/{vc.go => jsonld.go} | 51 ++++++ v1/vc/jsonld_test.go | 47 ++++++ v1/vc/jwt.go | 51 ++++++ v1/vc/vc_test.go | 44 ----- ...el.gen.go => verifiable_credential.gen.go} | 27 +++- 15 files changed, 506 insertions(+), 149 deletions(-) create mode 100644 cmd/jsonld.go create mode 100644 cmd/jwt.go create mode 100644 v1/did/model.gen.go create mode 100644 v1/vc/credential_subject.gen.go create mode 100644 v1/vc/issuer.gen.go rename v1/vc/{vc.go => jsonld.go} (50%) create mode 100644 v1/vc/jsonld_test.go create mode 100644 v1/vc/jwt.go delete mode 100644 v1/vc/vc_test.go rename v1/vc/{model.gen.go => verifiable_credential.gen.go} (74%) diff --git a/cmd/didcore.go b/cmd/didcore.go index eb5f6fa..8bb4481 100644 --- a/cmd/didcore.go +++ b/cmd/didcore.go @@ -1,18 +1,20 @@ package main -func didDocument() TypeDefinition { - return TypeDefinition{ +func didDocument() ModelDefinition { + return ModelDefinition{ Name: "Document", + Imports: []string{ + `"github.com/nuts-foundation/go-did/v1/ld"`, + `ssi "github.com/nuts-foundation/go-did"`, + `"github.com/nuts-foundation/go-did/did"`, + `"github.com/nuts-foundation/go-did/v1/ld"`, + }, Fields: []FieldDefinition{ - { - Name: "Context", - JSONName: "@context", - GoType: "[]interface", - }, { Name: "ID", JSONName: "id", GoType: "DID", + Required: true, }, { Name: "AlsoKnownAs", diff --git a/cmd/jsonld.go b/cmd/jsonld.go new file mode 100644 index 0000000..e08c4b8 --- /dev/null +++ b/cmd/jsonld.go @@ -0,0 +1,50 @@ +package main + +func generateLDSerializer(def ModelDefinition, implType string) string { + buf := "var _ " + def.Name + " = &" + implType + "{}\n" + buf += "\n" + buf += "type " + implType + " struct {\n" + buf += "\tld.Object\n" + buf += "\tcontext []interface{}\n" + buf += "}\n" + buf += "\n" + // Type-specific fields + for _, field := range def.Fields { + returnType := field.GoType + if !field.Required { + returnType = "(bool, " + field.GoType + ")" + } + buf += "func (o " + implType + ") " + field.Name + "() " + returnType + " {\n" + if field.Name == "Context" { + // Fixed Context field + buf += "\treturn o.context\n" + } else { + if field.Required { + buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" + buf += "\tif !ok {\n" + buf += "\t\treturn " + ldNilValue(field.GoType) + "\n" + buf += "\t}\n" + buf += "\treturn " + converterFunc(field.GoType) + "(obj)\n" + } else { + buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" + buf += "\tif !ok {\n" + buf += "\t\treturn false, " + converterFunc(field.GoType) + "(nil)\n" + buf += "\t}\n" + buf += "\treturn true, " + converterFunc(field.GoType) + "(obj)\n" + } + } + buf += "}\n\n" + } + return buf +} + +func ldNilValue(goType string) string { + switch goType { + case "ld.IDObject": + return "ld.IDObject{}" + case "time.Time": + return "time.Time{}" + default: + return "nil" + } +} diff --git a/cmd/jwt.go b/cmd/jwt.go new file mode 100644 index 0000000..3138fad --- /dev/null +++ b/cmd/jwt.go @@ -0,0 +1,11 @@ +package main + +func generateJWTSerializer(def ModelDefinition, implType string) string { + buf := "var _ " + def.Name + " = &" + implType + "{}\n" + buf += "\n" + buf += "type " + implType + " struct {\n" + buf += "\ttoken jwt.Token\n" + buf += "}\n" + buf += "\n" + return buf +} diff --git a/cmd/main.go b/cmd/main.go index 1df5852..81ea621 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,43 +2,78 @@ package main import ( "os" + "strings" ) func main() { - err := os.WriteFile("../v1/vc/model.gen.go", []byte(generate("vc", verifiableCredential())), 0644) - if err != nil { - panic(err) + targets := []struct { + file string + pkg string + def ModelDefinition + }{ + { + file: "../v1/vc/verifiable_credential.gen.go", + pkg: "vc", + def: verifiableCredential(), + }, + { + file: "../v1/vc/issuer.gen.go", + pkg: "vc", + def: issuer(), + }, + { + file: "../v1/vc/credential_subject.gen.go", + pkg: "vc", + def: credentialSubject(), + }, + { + file: "../v1/did/model.gen.go", + pkg: "did", + def: didDocument(), + }, + } + for _, target := range targets { + err := os.WriteFile(target.file, []byte(generate(target.pkg, target.def)), 0644) + if err != nil { + panic(err) + } } } -type TypeDefinition struct { - Name string - Fields []FieldDefinition +type ModelDefinition struct { + Name string + Fields []FieldDefinition + Imports []string + SupportLDSerialization bool + SupportJWTSerialization bool } type FieldDefinition struct { Name string JSONName string IRI string + JWTClaim string Required bool DocLink string GoType string } -func generate(pkg string, def TypeDefinition) string { - implType := "LD" + def.Name +func generate(pkg string, def ModelDefinition) string { buf := "" buf += "package " + pkg + "\n\n" buf += "\n" - buf += `import ( - "github.com/nuts-foundation/go-did/v1/ld" - "time" -)` - buf += "\n\n" + // Imports + buf += "import (\n" + for _, imp := range def.Imports { + buf += "\t" + imp + "\n" + } + if def.SupportJWTSerialization { + buf += "\t\"github.com/lestrrat-go/jwx/v2/jwt\"\n" + } + buf += ")\n" + buf += "\n" // Interface type buf += "type " + def.Name + " interface {\n" - buf += "\tld.Object\n" - buf += "\tContext() []interface{}\n" for _, field := range def.Fields { buf += "\t// " + field.Name + " as defined by " + field.DocLink + "\n" if field.Required { @@ -49,69 +84,39 @@ func generate(pkg string, def TypeDefinition) string { } buf += "}\n" buf += "\n" - // Implementation type - buf += "var _ " + def.Name + " = &" + implType + "{}\n" - buf += "\n" - buf += "type " + implType + " struct {\n" - buf += "\tld.Object\n" - buf += "\tcontext []interface{}\n" - buf += "}\n" - buf += "\n" - // Fixed Context field - buf += "func (o " + implType + ") Context() []interface{} {\n" - buf += "\treturn o.context\n" - buf += "}\n\n" - // Type-specific fields - for _, field := range def.Fields { - returnType := field.GoType - if !field.Required { - returnType = "(bool, " + field.GoType + ")" - } - buf += "func (o " + implType + ") " + field.Name + "() " + returnType + " {\n" - if field.Required { - buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" - buf += "\tif !ok {\n" - buf += "\t\treturn " + nilValue(field.GoType) + "\n" - buf += "\t}\n" - buf += "\treturn " + converterFunc(field.GoType) + "(obj)\n" - } else { - buf += "\tok, obj := o.Get(\"" + field.IRI + "\")\n" - buf += "\tif !ok {\n" - buf += "\t\treturn false, " + converterFunc(field.GoType) + "(nil)\n" - buf += "\t}\n" - buf += "\treturn true, " + converterFunc(field.GoType) + "(obj)\n" - } - buf += "}\n\n" + if def.SupportLDSerialization { + buf += generateLDSerializer(def, "LD"+def.Name) + } + if def.SupportJWTSerialization { + buf += generateJWTSerializer(def, "JWT"+def.Name) } return buf } -func nilValue(goType string) string { - switch goType { - case "ld.IDObject": - return "ld.IDObject{}" - case "time.Time": - return "time.Time{}" - default: - return "nil" +func converterFunc(goType string) string { + isSlice := goType[0] == '[' + parts := strings.Split(goType, ".") + name := parts[len(parts)-1] + // Remove non-alphanumeric characters + name = strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + return r + } + return -1 + }, name) + + var pkg string + if len(parts) > 1 || strings.ToLower(name) == name { + pkg = "ld" } -} -func converterFunc(goType string) string { - switch goType { - case "ld.Object": - return "ld.ToObject" - case "[]ld.Object": - return "ld.ToObjects" - case "ld.IDObject": - return "ld.NewIDObject" - case "time.Time": - return "ld.ToTime" - case "[]string": - return "ld.ToStrings" - case "[]interface{}": - return "ld.ToInterfaces" - default: - return "MISSING_CONVERTER" + if isSlice { + name += "s" + } + // First character to upper + name = "To" + strings.ToUpper(name[:1]) + name[1:] + if pkg != "" { + name = pkg + "." + name } + return name } diff --git a/cmd/vc.go b/cmd/vc.go index d0a3c56..97fdd5b 100644 --- a/cmd/vc.go +++ b/cmd/vc.go @@ -1,21 +1,44 @@ package main -func verifiableCredential() TypeDefinition { - return TypeDefinition{ - Name: "VerifiableCredential", +func verifiableCredential() ModelDefinition { + return ModelDefinition{ + Name: "VerifiableCredential", + SupportJWTSerialization: true, + SupportLDSerialization: true, + Imports: []string{ + `"github.com/nuts-foundation/go-did/v1/ld"`, + `"time"`, + `"net/url"`, + }, Fields: []FieldDefinition{ + { + Name: "Context", + GoType: "[]interface{}", + Required: true, + IRI: "@context", + JWTClaim: "vc.@context", + DocLink: "https://www.w3.org/TR/vc-data-model/#context-urls", + }, + { + Name: "ID", + GoType: "*url.URL", + IRI: "@id", + JWTClaim: "jti", + }, { Name: "Type", GoType: "[]string", Required: true, IRI: "@type", + JWTClaim: "vc.@type", DocLink: "https://www.w3.org/TR/vc-data-model/#types", }, { Name: "Issuer", - GoType: "ld.IDObject", + GoType: "Issuer", Required: true, IRI: "https://www.w3.org/2018/credentials#issuer", + JWTClaim: "iss", DocLink: "https://www.w3.org/TR/vc-data-model/#issuer", }, { @@ -23,6 +46,7 @@ func verifiableCredential() TypeDefinition { GoType: "time.Time", Required: true, IRI: "https://www.w3.org/2018/credentials#issuanceDate", + JWTClaim: "nbf", DocLink: "https://www.w3.org/TR/vc-data-model/#issuance", }, { @@ -30,15 +54,56 @@ func verifiableCredential() TypeDefinition { GoType: "time.Time", Required: false, IRI: "https://www.w3.org/2018/credentials#expirationDate", + JWTClaim: "exp", DocLink: "https://www.w3.org/TR/vc-data-model/#expiration", }, { Name: "CredentialSubject", - GoType: "[]ld.Object", + GoType: "[]CredentialSubject", Required: true, IRI: "https://www.w3.org/2018/credentials#credentialSubject", + JWTClaim: "vc.credentialSubject", DocLink: "https://www.w3.org/TR/vc-data-model/#credential-subject", }, }, } } + +func issuer() ModelDefinition { + return ModelDefinition{ + Name: "Issuer", + Imports: []string{ + `"github.com/nuts-foundation/go-did/v1/ld"`, + `"net/url"`, + }, + SupportLDSerialization: true, + SupportJWTSerialization: true, + Fields: []FieldDefinition{ + { + Name: "ID", + GoType: "*url.URL", + IRI: "@id", + Required: true, + }, + }, + } +} + +func credentialSubject() ModelDefinition { + return ModelDefinition{ + Name: "CredentialSubject", + Imports: []string{ + `"github.com/nuts-foundation/go-did/v1/ld"`, + `"net/url"`, + }, + SupportLDSerialization: true, + SupportJWTSerialization: true, + Fields: []FieldDefinition{ + { + Name: "ID", + GoType: "*url.URL", + IRI: "@id", + }, + }, + } +} diff --git a/v1/did/model.gen.go b/v1/did/model.gen.go new file mode 100644 index 0000000..24466d8 --- /dev/null +++ b/v1/did/model.gen.go @@ -0,0 +1,31 @@ +package did + + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/go-did/v1/ld" +) + +type Document interface { + // ID as defined by + ID() DID + // AlsoKnownAs as defined by + AlsoKnownAs() (bool, []ssi.URI) + // VerificationMethod as defined by + VerificationMethod() (bool, VerificationMethods) + // Authentication as defined by + Authentication() (bool, VerificationRelationships) + // AssertionMethod as defined by + AssertionMethod() (bool, VerificationRelationships) + // KeyAgreement as defined by + KeyAgreement() (bool, VerificationRelationships) + // CapabilityInvocation as defined by + CapabilityInvocation() (bool, VerificationRelationships) + // CapabilityDelegation as defined by + CapabilityDelegation() (bool, VerificationRelationships) + // Service as defined by + Service() (bool, []Service) +} + diff --git a/v1/ld/converters.go b/v1/ld/converters.go index d8911c3..e0380af 100644 --- a/v1/ld/converters.go +++ b/v1/ld/converters.go @@ -27,14 +27,6 @@ func ToTime(obj interface{}) time.Time { return result } -func NewIDObject(obj interface{}) IDObject { - return IDObject{ - map[string]interface{}{ - "@id": obj, - }, - } -} - func getValue(input interface{}) interface{} { asSlice, ok := input.([]interface{}) if !ok || len(asSlice) == 0 { diff --git a/v1/ld/model.go b/v1/ld/model.go index 8f80810..f10c623 100644 --- a/v1/ld/model.go +++ b/v1/ld/model.go @@ -2,10 +2,14 @@ package ld import "net/url" +type IDObject interface { + Object + ID() (bool, *url.URL) +} + type Object interface { Set(string, interface{}) error Get(string) (bool, interface{}) - ID() (bool, *url.URL) } var _ Object = &BaseObject{} @@ -34,15 +38,26 @@ func (o BaseObject) Get(s string) (bool, interface{}) { return ok, v } -// IDObject is an Object which is guaranteed to have an ID property. -type IDObject struct { +// IDContainer is an Object which is guaranteed to have an ID property. +type IDContainer struct { BaseObject } -func (U IDObject) ID() *url.URL { +func (U IDContainer) ID() *url.URL { ok, u := U.BaseObject.ID() if !ok { return &url.URL{} } return u } + +func ToURL(obj interface{}) *url.URL { + if obj == nil { + return &url.URL{} + } + u, err := url.Parse(obj.(string)) + if err != nil { + return &url.URL{} + } + return u +} diff --git a/v1/vc/credential_subject.gen.go b/v1/vc/credential_subject.gen.go new file mode 100644 index 0000000..c5d85b5 --- /dev/null +++ b/v1/vc/credential_subject.gen.go @@ -0,0 +1,35 @@ +package vc + + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + "net/url" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +type CredentialSubject interface { + // ID as defined by + ID() (bool, *url.URL) +} + +var _ CredentialSubject = &LDCredentialSubject{} + +type LDCredentialSubject struct { + ld.Object + context []interface{} +} + +func (o LDCredentialSubject) ID() (bool, *url.URL) { + ok, obj := o.Get("@id") + if !ok { + return false, ld.ToURL(nil) + } + return true, ld.ToURL(obj) +} + +var _ CredentialSubject = &JWTCredentialSubject{} + +type JWTCredentialSubject struct { + token jwt.Token +} + diff --git a/v1/vc/issuer.gen.go b/v1/vc/issuer.gen.go new file mode 100644 index 0000000..431c578 --- /dev/null +++ b/v1/vc/issuer.gen.go @@ -0,0 +1,35 @@ +package vc + + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + "net/url" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +type Issuer interface { + // ID as defined by + ID() *url.URL +} + +var _ Issuer = &LDIssuer{} + +type LDIssuer struct { + ld.Object + context []interface{} +} + +func (o LDIssuer) ID() *url.URL { + ok, obj := o.Get("@id") + if !ok { + return nil + } + return ld.ToURL(obj) +} + +var _ Issuer = &JWTIssuer{} + +type JWTIssuer struct { + token jwt.Token +} + diff --git a/v1/vc/vc.go b/v1/vc/jsonld.go similarity index 50% rename from v1/vc/vc.go rename to v1/vc/jsonld.go index 5838da4..6779609 100644 --- a/v1/vc/vc.go +++ b/v1/vc/jsonld.go @@ -4,11 +4,32 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/nuts-foundation/go-did/v1/ld" libld "github.com/piprate/json-gold/ld" + "strings" ) func Parse(raw string, documentLoader ld.DocumentLoader) (VerifiableCredential, error) { + if strings.HasPrefix(raw, "ey") { + return parseJWT(raw) + } else { + // JSON-LD + return parseJSONLD(raw, documentLoader) + } +} + +func parseJWT(raw string) (VerifiableCredential, error) { + token, err := jwt.Parse([]byte(raw)) + if err != nil { + return nil, fmt.Errorf("jwt parse: %w", err) + } + return JWTVerifiableCredential{ + token: token, + } +} + +func parseJSONLD(raw string, documentLoader ld.DocumentLoader) (VerifiableCredential, error) { document, err := libld.DocumentFromReader(bytes.NewReader([]byte(raw))) if err != nil { return nil, fmt.Errorf("jsonld read: %w", err) @@ -40,3 +61,33 @@ func unmarshalContext(input []byte) []interface{} { _ = json.Unmarshal(input, &c) return c.Context } + +func ToIssuer(obj interface{}) Issuer { + asSlice, ok := obj.([]interface{}) + if !ok { + return nil + } + // should be only 1 + for _, curr := range asSlice { + return &LDIssuer{ + Object: ld.ToObject(curr).(ld.BaseObject), + context: nil, + } + } + return nil +} + +func ToCredentialSubjects(obj interface{}) []CredentialSubject { + asSlice, ok := obj.([]interface{}) + if !ok { + return nil + } + var result []CredentialSubject + for _, raw := range asSlice { + result = append(result, &LDCredentialSubject{ + Object: ld.ToObject(raw).(ld.BaseObject), + context: nil, + }) + } + return result +} diff --git a/v1/vc/jsonld_test.go b/v1/vc/jsonld_test.go new file mode 100644 index 0000000..d36e10a --- /dev/null +++ b/v1/vc/jsonld_test.go @@ -0,0 +1,47 @@ +package vc + +import ( + "github.com/nuts-foundation/go-did/v1/ld" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestParse(t *testing.T) { + t.Run("JSON-LD", func(t *testing.T) { + data, err := os.ReadFile("test/example7.json") + require.NoError(t, err) + + actual, err := Parse(string(data), ld.Loader()) + + require.NoError(t, err) + require.NotNil(t, actual) + + // ID + ok, id := actual.ID() + require.True(t, ok) + require.Equal(t, "http://example.edu/credentials/3732", id.String()) + // Issuer + require.Equal(t, "https://example.com/issuer/123", actual.Issuer().ID().String()) + // IssuanceDate + require.Equal(t, "2010-01-01 00:00:00 +0000 UTC", actual.IssuanceDate().String()) + // Type + require.Len(t, actual.Type(), 2) + require.Contains(t, actual.Type(), "https://www.w3.org/2018/credentials#VerifiableCredential") + require.Contains(t, actual.Type(), "https://example.org/examples#RelationshipCredential") + // Context + require.Len(t, actual.Context(), 2) + require.Equal(t, "https://www.w3.org/2018/credentials/v1", actual.Context()[0]) + require.Equal(t, "https://www.w3.org/2018/credentials/examples/v1", actual.Context()[1]) + // CredentialSubject + subjects := actual.CredentialSubject() + require.Len(t, subjects, 2) + { + subject := subjects[0] + ok, id := subject.ID() + require.True(t, ok) + require.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", id.String()) + } + }) + +} diff --git a/v1/vc/jwt.go b/v1/vc/jwt.go new file mode 100644 index 0000000..c69ab26 --- /dev/null +++ b/v1/vc/jwt.go @@ -0,0 +1,51 @@ +package vc + +import ( + "github.com/lestrrat-go/jwx/v2/jwt" + "net/url" + "time" +) + +var _ VerifiableCredential = &JWTVerifiableCredential{} + +type JWTVerifiableCredential struct { + token jwt.Token +} + +func (j JWTVerifiableCredential) Context() []interface{} { + //TODO implement me + panic("implement me") +} + +func (j JWTVerifiableCredential) ID() (bool, *url.URL) { + id := j.token.JwtID() + if id == "" { + return false, nil + } + result, _ := url.Parse(id) + return result != nil, result +} + +func (j JWTVerifiableCredential) Type() []string { + vc, ok := j.token.Get("vc") +} + +func (j JWTVerifiableCredential) Issuer() Issuer { + //TODO implement me + panic("implement me") +} + +func (j JWTVerifiableCredential) IssuanceDate() time.Time { + //TODO implement me + panic("implement me") +} + +func (j JWTVerifiableCredential) ExpirationDate() (bool, time.Time) { + //TODO implement me + panic("implement me") +} + +func (j JWTVerifiableCredential) CredentialSubject() []CredentialSubject { + //TODO implement me + panic("implement me") +} diff --git a/v1/vc/vc_test.go b/v1/vc/vc_test.go deleted file mode 100644 index a7decdf..0000000 --- a/v1/vc/vc_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package vc - -import ( - "github.com/nuts-foundation/go-did/v1/ld" - "github.com/stretchr/testify/require" - "os" - "testing" -) - -func TestParse(t *testing.T) { - data, err := os.ReadFile("test/example7.json") - require.NoError(t, err) - - actual, err := Parse(string(data), ld.Loader()) - - require.NoError(t, err) - require.NotNil(t, actual) - - // ID - ok, id := actual.ID() - require.True(t, ok) - require.Equal(t, "http://example.edu/credentials/3732", id.ID().String()) - // IssuanceDate - require.Equal(t, "2010-01-01 00:00:00 +0000 UTC", actual.IssuanceDate().String()) - // Type - require.Len(t, actual.Type(), 2) - require.Contains(t, actual.Type(), "https://www.w3.org/2018/credentials#VerifiableCredential") - require.Contains(t, actual.Type(), "https://example.org/examples#RelationshipCredential") - // Context - require.Len(t, actual.Context(), 2) - require.Equal(t, "https://www.w3.org/2018/credentials/v1", actual.Context()[0]) - require.Equal(t, "https://www.w3.org/2018/credentials/examples/v1", actual.Context()[1]) - // CredentialSubject - subjects := actual.CredentialSubject() - require.Len(t, subjects, 2) - { - subject := subjects[0] - ok, id := subject.ID() - require.True(t, ok) - require.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", id.String()) - subject.Get() - } - -} diff --git a/v1/vc/model.gen.go b/v1/vc/verifiable_credential.gen.go similarity index 74% rename from v1/vc/model.gen.go rename to v1/vc/verifiable_credential.gen.go index ab34f1f..98aa4e3 100644 --- a/v1/vc/model.gen.go +++ b/v1/vc/verifiable_credential.gen.go @@ -2,22 +2,25 @@ package vc import ( "github.com/nuts-foundation/go-did/v1/ld" + "net/url" "time" ) type VerifiableCredential interface { - ld.Object + // Context as defined by https://www.w3.org/TR/vc-data-model/#context-urls Context() []interface{} + // ID as defined by + ID() (bool, *url.URL) // Type as defined by https://www.w3.org/TR/vc-data-model/#types Type() []string // Issuer as defined by https://www.w3.org/TR/vc-data-model/#issuer - Issuer() ld.IDObject + Issuer() Issuer // IssuanceDate as defined by https://www.w3.org/TR/vc-data-model/#issuance IssuanceDate() time.Time // ExpirationDate as defined by https://www.w3.org/TR/vc-data-model/#expiration ExpirationDate() (bool, time.Time) // CredentialSubject as defined by https://www.w3.org/TR/vc-data-model/#credential-subject - CredentialSubject() []ld.Object + CredentialSubject() []CredentialSubject } var _ VerifiableCredential = &LDVerifiableCredential{} @@ -31,6 +34,14 @@ func (o LDVerifiableCredential) Context() []interface{} { return o.context } +func (o LDVerifiableCredential) ID() (bool, *url.URL) { + ok, obj := o.Get("@id") + if !ok { + return false, ld.ToURL(nil) + } + return true, ld.ToURL(obj) +} + func (o LDVerifiableCredential) Type() []string { ok, obj := o.Get("@type") if !ok { @@ -39,12 +50,12 @@ func (o LDVerifiableCredential) Type() []string { return ld.ToStrings(obj) } -func (o LDVerifiableCredential) Issuer() ld.IDObject { +func (o LDVerifiableCredential) Issuer() Issuer { ok, obj := o.Get("https://www.w3.org/2018/credentials#issuer") if !ok { - return ld.IDObject{} + return nil } - return ld.NewIDObject(obj) + return ToIssuer(obj) } func (o LDVerifiableCredential) IssuanceDate() time.Time { @@ -63,10 +74,10 @@ func (o LDVerifiableCredential) ExpirationDate() (bool, time.Time) { return true, ld.ToTime(obj) } -func (o LDVerifiableCredential) CredentialSubject() []ld.Object { +func (o LDVerifiableCredential) CredentialSubject() []CredentialSubject { ok, obj := o.Get("https://www.w3.org/2018/credentials#credentialSubject") if !ok { return nil } - return ld.ToObjects(obj) + return ToCredentialSubjects(obj) }