Skip to content

Commit

Permalink
Support VCs and VPs in JWT format (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Oct 10, 2023
1 parent a79acb0 commit 22462b1
Show file tree
Hide file tree
Showing 7 changed files with 513 additions and 115 deletions.
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,20 @@

A library to parse and generate W3C [DID Documents](https://www.w3.org/TR/did-core/) and W3C [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/).

## Example usage:
## Example usage
Note on parsing: in earlier versions, DID documents, credentials and presentations were parsed using `UnmarshalJSON`.
Now, `ParseDocument()`, `ParseVerifiableCredential()` and `ParseVerifiablePresentation()` should be used instead: they better support VCs and VPs in JWT format.

### Parsing a DID document
```go
didDoc, err := did.ParseDocument(didDocJson)
if err != nil {
panic(err)
}
// do something with didDoc
````

### Creating a DID document
Creation of a simple DID Document which is its own controller and contains an AssertionMethod.
```go
didID, err := did.ParseDID("did:example:123")
Expand All @@ -29,18 +42,8 @@ doc.AddAssertionMethod(verificationMethod)
didJson, _ := json.MarshalIndent(doc, "", " ")
fmt.Println(string(didJson))

// Unmarshalling of a json did document:
parsedDIDDoc := did.Document{}
err = json.Unmarshal(didJson, &parsedDIDDoc)

// It can return the key in the convenient lestrrat-go/jwx JWK
parsedDIDDoc.AssertionMethod[0].JWK()

// Or return a native crypto.PublicKey
parsedDIDDoc.AssertionMethod[0].PublicKey()

```

Outputs:
```json
{
Expand All @@ -64,14 +67,16 @@ Outputs:
}
]
}

```

### Parsing Verifiable Credentials and Verifiable Presentations
The library supports parsing of Verifiable Credentials and Verifiable Presentations in JSON-LD, and JWT proof format.
Use `ParseVerifiableCredential(raw string)` and `ParseVerifiablePresentation(raw string)`.

## Installation
```
go get github.com/nuts-foundation/go-did
```
## State of the library
Currently, the library is under development. The api can change without notice.
Checkout the issues and PRs to be informed about any development.
We keep the API stable, breaking changes will only be introduced in new major versions.
60 changes: 35 additions & 25 deletions did/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ import (
"github.com/nuts-foundation/go-did/internal/marshal"
)

// ParseDocument parses a DID Document from a string.
func ParseDocument(raw string) (*Document, error) {
type Alias Document
normalizedDoc, err := marshal.NormalizeDocument([]byte(raw), pluralContext, marshal.Plural(controllerKey))
if err != nil {
return nil, err
}
doc := Alias{}
err = json.Unmarshal(normalizedDoc, &doc)
if err != nil {
return nil, err
}
d := Document(doc)

const errMsg = "unable to resolve all '%s' references: %w"
if err = resolveVerificationRelationships(d.Authentication, d.VerificationMethod); err != nil {
return nil, fmt.Errorf(errMsg, authenticationKey, err)
}
if err = resolveVerificationRelationships(d.AssertionMethod, d.VerificationMethod); err != nil {
return nil, fmt.Errorf(errMsg, assertionMethodKey, err)
}
if err = resolveVerificationRelationships(d.KeyAgreement, d.VerificationMethod); err != nil {
return nil, fmt.Errorf(errMsg, keyAgreementKey, err)
}
if err = resolveVerificationRelationships(d.CapabilityInvocation, d.VerificationMethod); err != nil {
return nil, fmt.Errorf(errMsg, capabilityInvocationKey, err)
}
if err = resolveVerificationRelationships(d.CapabilityDelegation, d.VerificationMethod); err != nil {
return nil, fmt.Errorf(errMsg, capabilityDelegationKey, err)
}
return &d, nil
}

// Document represents a DID Document as specified by the DID Core specification (https://www.w3.org/TR/did-core/).
type Document struct {
Context []ssi.URI `json:"@context"`
Expand Down Expand Up @@ -186,34 +219,11 @@ func (d Document) MarshalJSON() ([]byte, error) {
}

func (d *Document) UnmarshalJSON(b []byte) error {
type Alias Document
normalizedDoc, err := marshal.NormalizeDocument(b, pluralContext, marshal.Plural(controllerKey))
if err != nil {
return err
}
doc := Alias{}
err = json.Unmarshal(normalizedDoc, &doc)
document, err := ParseDocument(string(b))
if err != nil {
return err
}
*d = (Document)(doc)

const errMsg = "unable to resolve all '%s' references: %w"
if err = resolveVerificationRelationships(d.Authentication, d.VerificationMethod); err != nil {
return fmt.Errorf(errMsg, authenticationKey, err)
}
if err = resolveVerificationRelationships(d.AssertionMethod, d.VerificationMethod); err != nil {
return fmt.Errorf(errMsg, assertionMethodKey, err)
}
if err = resolveVerificationRelationships(d.KeyAgreement, d.VerificationMethod); err != nil {
return fmt.Errorf(errMsg, keyAgreementKey, err)
}
if err = resolveVerificationRelationships(d.CapabilityInvocation, d.VerificationMethod); err != nil {
return fmt.Errorf(errMsg, capabilityInvocationKey, err)
}
if err = resolveVerificationRelationships(d.CapabilityDelegation, d.VerificationMethod); err != nil {
return fmt.Errorf(errMsg, capabilityDelegationKey, err)
}
*d = *document
return nil
}

Expand Down
3 changes: 1 addition & 2 deletions did/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,7 @@ func TestRoundTripMarshalling(t *testing.T) {

for _, testCase := range testCases {
t.Run(testCase, func(t *testing.T) {
document := Document{}
err := json.Unmarshal(test.ReadTestFile("test/"+testCase+".json"), &document)
document, err := ParseDocument(string(test.ReadTestFile("test/" + testCase + ".json")))
if !assert.NoError(t, err) {
return
}
Expand Down
136 changes: 125 additions & 11 deletions vc/vc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/jwt"
"github.com/nuts-foundation/go-did/did"
"strings"
"time"

ssi "github.com/nuts-foundation/go-did"
Expand Down Expand Up @@ -32,6 +34,94 @@ func VCContextV1URI() ssi.URI {
}
}

const (
// JSONLDCredentialProofFormat is the format for JSON-LD based credentials.
JSONLDCredentialProofFormat string = "ldp_vc"
// JWTCredentialsProofFormat is the format for JWT based credentials.
// Note: various specs have not yet decided on the exact const (jwt_vc or jwt_vc_json, etc), so this is subject to change.
JWTCredentialsProofFormat = "jwt_vc"
)

var errCredentialSubjectWithoutID = errors.New("credential subjects have no ID")

// ParseVerifiableCredential parses a Verifiable Credential from a string, which can be either in JSON-LD or JWT format.
// JWTs are parsed according to https://www.w3.org/TR/2022/REC-vc-data-model-20220303/#jwt-decoding
// If the format is JWT, the parsed token can be retrieved using JWT().
// Note that it does not do any signature checking.
func ParseVerifiableCredential(raw string) (*VerifiableCredential, error) {
raw = strings.TrimSpace(raw)
if strings.HasPrefix(raw, "{") {
// Assume JSON-LD format
return parseJSONLDCredential(raw)
} else {
// Assume JWT format
return parseJWTCredential(raw)
}
}

// parseJWTCredential parses a JWT credential according to https://www.w3.org/TR/2022/REC-vc-data-model-20220303/#jwt-decoding
func parseJWTCredential(raw string) (*VerifiableCredential, error) {
token, err := jwt.Parse([]byte(raw))
if err != nil {
return nil, err
}
var result VerifiableCredential
if innerVCInterf := token.PrivateClaims()["vc"]; innerVCInterf != nil {
innerVCJSON, _ := json.Marshal(innerVCInterf)
err = json.Unmarshal(innerVCJSON, &result)
if err != nil {
return nil, fmt.Errorf("invalid JWT 'vc' claim: %w", err)
}
}
// parse exp
exp := token.Expiration()
result.ExpirationDate = &exp
// parse iss
if iss, err := parseURIClaim(token, jwt.IssuerKey); err != nil {
return nil, err
} else if iss != nil {
result.Issuer = *iss
}
// parse nbf
result.IssuanceDate = token.NotBefore()
// parse sub
if token.Subject() != "" {
for _, credentialSubjectInterf := range result.CredentialSubject {
credentialSubject, isMap := credentialSubjectInterf.(map[string]interface{})
if isMap {
credentialSubject["id"] = token.Subject()
}
}
}
// parse jti
if jti, err := parseURIClaim(token, jwt.JwtIDKey); err != nil {
return nil, err
} else if jti != nil {
result.ID = jti
}
result.format = JWTCredentialsProofFormat
result.raw = raw
result.token = token
return &result, nil
}

func parseJSONLDCredential(raw string) (*VerifiableCredential, error) {
type Alias VerifiableCredential
normalizedVC, err := marshal.NormalizeDocument([]byte(raw), pluralContext, marshal.Plural(typeKey), marshal.Plural(credentialSubjectKey), marshal.Plural(proofKey))
if err != nil {
return nil, err
}
alias := Alias{}
err = json.Unmarshal(normalizedVC, &alias)
if err != nil {
return nil, err
}
alias.format = JSONLDCredentialProofFormat
alias.raw = raw
result := VerifiableCredential(alias)
return &result, err
}

// VerifiableCredential represents a credential as defined by the Verifiable Credentials Data Model 1.0 specification (https://www.w3.org/TR/vc-data-model/).
type VerifiableCredential struct {
// Context defines the json-ld context to dereference the URIs
Expand All @@ -52,6 +142,29 @@ type VerifiableCredential struct {
CredentialSubject []interface{} `json:"credentialSubject"`
// Proof contains the cryptographic proof(s). It must be extracted using the Proofs method or UnmarshalProofValue method for non-generic proof fields.
Proof []interface{} `json:"proof"`

format string
raw string
token jwt.Token
}

// Format returns the format of the credential (e.g. jwt_vc or ldp_vc).
func (vc VerifiableCredential) Format() string {
return vc.format
}

// Raw returns the source of the credential as it was parsed.
func (vc VerifiableCredential) Raw() string {
return vc.raw
}

// JWT returns the JWT token if the credential was parsed from a JWT.
func (vc VerifiableCredential) JWT() jwt.Token {
if vc.token == nil {
return nil
}
token, _ := vc.token.Clone()
return token
}

// CredentialStatus defines the method on how to determine a credential is revoked.
Expand Down Expand Up @@ -87,18 +200,19 @@ func (vc VerifiableCredential) MarshalJSON() ([]byte, error) {
}

func (vc *VerifiableCredential) UnmarshalJSON(b []byte) error {
type Alias VerifiableCredential
normalizedVC, err := marshal.NormalizeDocument(b, pluralContext, marshal.Plural(typeKey), marshal.Plural(credentialSubjectKey), marshal.Plural(proofKey))
if err != nil {
return err
var str string
if len(b) > 0 && b[0] == '"' {
if err := json.Unmarshal(b, &str); err != nil {
return err
}
} else {
str = string(b)
}
tmp := Alias{}
err = json.Unmarshal(normalizedVC, &tmp)
if err != nil {
return err
credential, err := ParseVerifiableCredential(str)
if err == nil {
*vc = *credential
}
*vc = (VerifiableCredential)(tmp)
return nil
return err
}

// UnmarshalProofValue unmarshalls the proof to the given proof type. Always pass a slice as target since there could be multiple proofs.
Expand Down Expand Up @@ -147,7 +261,7 @@ func (vc VerifiableCredential) SubjectDID() (*did.DID, error) {
}
}
if subjectID.Empty() {
return nil, errors.New("unable to get subject DID from VC: credential subjects have no ID")
return nil, fmt.Errorf("unable to get subject DID from VC: %w", errCredentialSubjectWithoutID)
}
return &subjectID, nil
}
Expand Down
Loading

0 comments on commit 22462b1

Please sign in to comment.