Skip to content

Commit

Permalink
Verifying presentations
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Sep 29, 2023
1 parent c77e82b commit 8895eaa
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 73 deletions.
2 changes: 1 addition & 1 deletion crypto/jwx.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import (
// ErrUnsupportedSigningKey is returned when an unsupported private key is used to sign. Currently only ecdsa and rsa keys are supported
var ErrUnsupportedSigningKey = errors.New("signing key algorithm not supported")

var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.ES384, jwa.ES512}
var supportedAlgorithms = []jwa.SignatureAlgorithm{jwa.PS256, jwa.PS384, jwa.PS512, jwa.ES256, jwa.ES256K, jwa.ES384, jwa.ES512}

const defaultRsaEncryptionAlgorithm = jwa.RSA_OAEP_256
const defaultEcEncryptionAlgorithm = jwa.ECDH_ES_A256KW
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
github.com/nats-io/nats-server/v2 v2.10.1
github.com/nats-io/nats.go v1.30.1
github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776
github.com/nuts-foundation/go-did v0.6.6-0.20230929085947-a25b9c90bc7d
github.com/nuts-foundation/go-leia/v4 v4.0.0
github.com/nuts-foundation/go-stoabs v1.9.0
// check the oapi-codegen tool version in the makefile when upgrading the runtime
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,8 @@ github.com/nuts-foundation/go-did v0.6.6-0.20230929062946-723b4514eb2e h1:2Kmoby
github.com/nuts-foundation/go-did v0.6.6-0.20230929062946-723b4514eb2e/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776 h1:BJ2MyIj8U3mxplXzTLUVrUmStqlLVYA1LVrTFUqHZhE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929063840-997f267c2776/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-did v0.6.6-0.20230929085947-a25b9c90bc7d h1:GiyKibasdHpzNG1QuyHshn4w2MWdbXNE0/5Yrq2JLhY=
github.com/nuts-foundation/go-did v0.6.6-0.20230929085947-a25b9c90bc7d/go.mod h1:Jb3IgnO2Zeed970JMIlfjr4g1kvikmgWUJA0EfeDEFE=
github.com/nuts-foundation/go-leia/v4 v4.0.0 h1:/unYCk18qGG2HWcJK4ld4CaM6k7Tdr0bR1vQd1Jwfcg=
github.com/nuts-foundation/go-leia/v4 v4.0.0/go.mod h1:A246dA4nhY99OPCQpG/XbQ/iPyyfSaJchanivuPWpao=
github.com/nuts-foundation/go-stoabs v1.9.0 h1:zK+ugfolaJYyBvGwsRuavLVdycXk4Yw/1gI+tz17lWQ=
Expand Down
6 changes: 4 additions & 2 deletions vcr/issuer/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ type CredentialSearcher interface {
}

const (
JSONLDCredentialFormat = vc.JSONLDCredentialProofFormat
JWTCredentialFormat = vc.JWTCredentialsProofFormat
JSONLDCredentialFormat = vc.JSONLDCredentialProofFormat
JWTCredentialFormat = vc.JWTCredentialsProofFormat
JSONLDPresentationFormat = vc.JSONLDPresentationProofFormat
JWTPresentationFormat = vc.JWTPresentationProofFormat
)

// CredentialOptions specifies options for issuing a credential.
Expand Down
65 changes: 33 additions & 32 deletions vcr/issuer/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (i issuer) buildVC(ctx context.Context, template vc.VerifiableCredential, o

switch options.Format {
case JWTCredentialFormat:
return i.buildJWTCredential(ctx, unsignedCredential, key)
return BuildJWTCredential(ctx, i.keyStore, unsignedCredential, key)
case "":
fallthrough
case JSONLDCredentialFormat:
Expand All @@ -236,37 +236,6 @@ func (i issuer) buildVC(ctx context.Context, template vc.VerifiableCredential, o
}
}

func (i issuer) buildJWTCredential(ctx context.Context, template vc.VerifiableCredential, key crypto.Key) (*vc.VerifiableCredential, error) {
subjectDID, err := template.SubjectDID()
if err != nil {
return nil, err
}
headers := map[string]interface{}{
jws.TypeKey: "JWT",
}
claims := map[string]interface{}{
jwt.NotBeforeKey: template.IssuanceDate,
jwt.IssuerKey: template.Issuer.String(),
jwt.SubjectKey: subjectDID.String(),
"vc": vc.VerifiableCredential{
Context: template.Context,
Type: template.Type,
CredentialSubject: template.CredentialSubject,
},
}
if template.ID != nil {
claims[jwt.JwtIDKey] = template.ID.String()
}
if template.ExpirationDate != nil {
claims[jwt.ExpirationKey] = *template.ExpirationDate
}
token, err := i.keyStore.SignJWT(ctx, claims, headers, key)
if err != nil {
return nil, fmt.Errorf("unable to sign JWT credential: %w", err)
}
return vc.ParseVerifiableCredential(token)
}

func (i issuer) buildJSONLDCredential(ctx context.Context, unsignedCredential vc.VerifiableCredential, key crypto.Key) (*vc.VerifiableCredential, error) {
credentialAsMap := map[string]interface{}{}
b, _ := json.Marshal(unsignedCredential)
Expand Down Expand Up @@ -382,3 +351,35 @@ func (i issuer) isRevoked(credentialID ssi.URI) (bool, error) {
func (i issuer) SearchCredential(credentialType ssi.URI, issuer did.DID, subject *ssi.URI) ([]vc.VerifiableCredential, error) {
return i.store.SearchCredential(credentialType, issuer, subject)
}

// BuildJWTCredential builds a JWT credential from the given template credential.
func BuildJWTCredential(ctx context.Context, signer crypto.JWTSigner, template vc.VerifiableCredential, key crypto.Key) (*vc.VerifiableCredential, error) {
subjectDID, err := template.SubjectDID()
if err != nil {
return nil, err
}
headers := map[string]interface{}{
jws.TypeKey: "JWT",
}
claims := map[string]interface{}{
jwt.NotBeforeKey: template.IssuanceDate,
jwt.IssuerKey: template.Issuer.String(),
jwt.SubjectKey: subjectDID.String(),
"vc": vc.VerifiableCredential{
Context: template.Context,
Type: template.Type,
CredentialSubject: template.CredentialSubject,
},
}
if template.ID != nil {
claims[jwt.JwtIDKey] = template.ID.String()
}
if template.ExpirationDate != nil {
claims[jwt.ExpirationKey] = *template.ExpirationDate
}
token, err := signer.SignJWT(ctx, claims, headers, key)
if err != nil {
return nil, fmt.Errorf("unable to sign JWT credential: %w", err)
}
return vc.ParseVerifiableCredential(token)
}
147 changes: 113 additions & 34 deletions vcr/verifier/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
package verifier

import (
crypt "crypto"
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
"strings"
"time"
Expand All @@ -43,6 +46,8 @@ const (
maxSkew = 5 * time.Second
)

var errVerificationMethodNotOfIssuer = errors.New("verification method is not of issuer")

// verifier implements the Verifier interface.
// It implements the generic methods for verifying verifiable credentials and verifiable presentations.
// It does not know anything about the semantics of a credential. It should support a wide range of types.
Expand Down Expand Up @@ -111,6 +116,17 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time
return err
}

switch credentialToVerify.Format() {
case issuer.JSONLDCredentialFormat:
return v.validateJSONLDCredential(credentialToVerify, at)
case issuer.JWTCredentialFormat:
return v.validateJWTCredential(credentialToVerify, at)
default:
return errors.New("unsupported credential proof format")
}
}

func (v *verifier) validateJSONLDCredential(credentialToVerify vc.VerifiableCredential, at *time.Time) error {
signedDocument, err := proof.NewSignedDocument(credentialToVerify)
if err != nil {
return fmt.Errorf("unable to build signed document from verifiable credential: %w", err)
Expand All @@ -124,10 +140,9 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time
verificationMethod := ldProof.VerificationMethod.String()
verificationMethodIssuer := strings.Split(verificationMethod, "#")[0]
if verificationMethodIssuer == "" || verificationMethodIssuer != credentialToVerify.Issuer.String() {
return errors.New("verification method is not of issuer")
return errVerificationMethodNotOfIssuer
}

// find key
pk, err := v.keyResolver.ResolveKeyByID(ldProof.VerificationMethod.String(), at, resolver.NutsSigningKeyType)
if err != nil {
if at == nil {
Expand All @@ -136,10 +151,36 @@ func (v *verifier) Validate(credentialToVerify vc.VerifiableCredential, at *time
return fmt.Errorf("unable to resolve valid signing key at given time: %w", err)
}

// Try first with the correct LDProof implementation
return ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: v.jsonldManager.DocumentLoader()}, pk)
}

func (v *verifier) validateJWTCredential(credential vc.VerifiableCredential, at *time.Time) error {
var keyID string
_, err := crypto.ParseJWT(credential.Raw(), func(kid string) (crypt.PublicKey, error) {
keyID = kid
return v.resolveSigningKey(kid, credential.Issuer.String(), at)
})
if err != nil {
return fmt.Errorf("unable to validate JWT credential: %w", err)
}
if keyID != "" && strings.Split(keyID, "#")[0] != credential.Issuer.String() {
return errVerificationMethodNotOfIssuer
}
return nil
}

func (v *verifier) resolveSigningKey(kid string, issuer string, at *time.Time) (crypt.PublicKey, error) {
// Compatibility: VC data model v1 puts key discovery out of scope and does not require the `kid` header.
// When `kid` isn't present use the JWT issuer as `kid`, then it is at least compatible with DID methods that contain a single verification method (did:jwk).
if kid == "" {
kid = issuer
}
if strings.HasPrefix(kid, "did:jwk:") && !strings.Contains(kid, "#") {
kid += "#0"
}
return v.keyResolver.ResolveKeyByID(kid, at, resolver.NutsSigningKeyType)
}

// Verify implements the verify interface.
// It currently checks if the credential has the required fields and values, if it is valid at the given time and optional the signature.
func (v verifier) Verify(credentialToVerify vc.VerifiableCredential, allowUntrusted bool, checkSignature bool, validAt *time.Time) error {
Expand Down Expand Up @@ -236,7 +277,7 @@ func (v *verifier) RegisterRevocation(revocation credential.Revocation) error {
vm := revocation.Proof.VerificationMethod.String()
vmIssuer := strings.Split(vm, "#")[0]
if vmIssuer != revocation.Issuer.String() {
return errors.New("verificationMethod should owned by the issuer")
return errVerificationMethodNotOfIssuer
}

pk, err := v.keyResolver.ResolveKeyByID(revocation.Proof.VerificationMethod.String(), &revocation.Date, resolver.NutsSigningKeyType)
Expand Down Expand Up @@ -264,48 +305,30 @@ func (v verifier) VerifyVP(vp vc.VerifiablePresentation, verifyVCs bool, allowUn
}

// doVerifyVP delegates VC verification to the supplied Verifier, to aid unit testing.
func (v verifier) doVerifyVP(vcVerifier Verifier, vp vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) {
// Multiple proofs might be supported in the future, when there's an actual use case.
if len(vp.Proof) != 1 {
return nil, newVerificationError("exactly 1 proof is expected")
}
// Make sure the proofs are LD-proofs
var ldProofs []proof.LDProof
err := vp.UnmarshalProofValue(&ldProofs)
if err != nil {
return nil, newVerificationError("unsupported proof type: %w", err)
func (v verifier) doVerifyVP(vcVerifier Verifier, presentation vc.VerifiablePresentation, verifyVCs bool, allowUntrustedVCs bool, validAt *time.Time) ([]vc.VerifiableCredential, error) {
var err error
switch presentation.Format() {
case issuer.JSONLDPresentationFormat:
err = v.validateJSONLDPresentation(presentation, validAt)
case issuer.JWTPresentationFormat:
err = v.validateJWTPresentation(presentation, validAt)
default:
err = errors.New("unsupported presentation proof format")
}
ldProof := ldProofs[0]

// Validate signing time
if !v.validateAtTime(ldProof.Created, ldProof.Expires, validAt) {
return nil, toVerificationError(types.ErrPresentationNotValidAtTime)
}

// Validate signature
signingKey, err := v.keyResolver.ResolveKeyByID(ldProof.VerificationMethod.String(), validAt, resolver.NutsSigningKeyType)
if err != nil {
return nil, fmt.Errorf("unable to resolve valid signing key: %w", err)
}
signedDocument, err := proof.NewSignedDocument(vp)
if err != nil {
return nil, newVerificationError("invalid LD-JSON document: %w", err)
}
err = ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: v.jsonldManager.DocumentLoader()}, signingKey)
if err != nil {
return nil, newVerificationError("invalid signature: %w", err)
return nil, err
}

if verifyVCs {
for _, current := range vp.VerifiableCredential {
for _, current := range presentation.VerifiableCredential {
err := vcVerifier.Verify(current, allowUntrustedVCs, true, validAt)
if err != nil {
return nil, newVerificationError("invalid VC (id=%s): %w", current.ID, err)
}
}
}

return vp.VerifiableCredential, nil
return presentation.VerifiableCredential, nil
}

func (v *verifier) validateType(credential vc.VerifiableCredential) error {
Expand All @@ -321,3 +344,59 @@ func (v *verifier) validateType(credential vc.VerifiableCredential) error {
}
return fmt.Errorf("verifiable credential does not list '%s' as type", vc.VerifiableCredentialTypeV1URI())
}

func (v *verifier) validateJSONLDPresentation(presentation vc.VerifiablePresentation, validAt *time.Time) error {
// Multiple proofs might be supported in the future, when there's an actual use case.
if len(presentation.Proof) != 1 {
return newVerificationError("exactly 1 proof is expected")
}
// Make sure the proofs are LD-proofs
var ldProofs []proof.LDProof
err := presentation.UnmarshalProofValue(&ldProofs)
if err != nil {
return newVerificationError("unsupported proof type: %w", err)
}
ldProof := ldProofs[0]

// Validate signing time
if !v.validateAtTime(ldProof.Created, ldProof.Expires, validAt) {
return toVerificationError(types.ErrPresentationNotValidAtTime)
}

// Validate signature
signingKey, err := v.keyResolver.ResolveKeyByID(ldProof.VerificationMethod.String(), validAt, resolver.NutsSigningKeyType)
if err != nil {
return fmt.Errorf("unable to resolve valid signing key: %w", err)
}
signedDocument, err := proof.NewSignedDocument(presentation)
if err != nil {
return newVerificationError("invalid LD-JSON document: %w", err)
}
err = ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: v.jsonldManager.DocumentLoader()}, signingKey)
if err != nil {
return newVerificationError("invalid signature: %w", err)
}
return nil
}

func (v *verifier) validateJWTPresentation(presentation vc.VerifiablePresentation, at *time.Time) error {
var keyID string
if len(presentation.VerifiableCredential) != 1 {
return errors.New("exactly 1 credential in JWT VP is expected")
}
subjectDID, err := presentation.VerifiableCredential[0].SubjectDID()
if err != nil {
return err
}
_, err = crypto.ParseJWT(presentation.Raw(), func(kid string) (crypt.PublicKey, error) {
keyID = kid
return v.resolveSigningKey(kid, subjectDID.String(), at)
})
if err != nil {
return fmt.Errorf("unable to validate JWT credential: %w", err)
}
if keyID != "" && strings.Split(keyID, "#")[0] != subjectDID.String() {
return errVerificationMethodNotOfIssuer
}
return nil
}
Loading

0 comments on commit 8895eaa

Please sign in to comment.