Skip to content

Commit

Permalink
split signature verification from verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
gerardsn committed Dec 13, 2023
1 parent 960ebe5 commit 1a0578e
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 470 deletions.
2 changes: 1 addition & 1 deletion jsonld/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (t testContextManager) Configure(config Config) error {
}

// NewTestJSONLDManager creates a new test context manager which contains extra test contexts
func NewTestJSONLDManager(t *testing.T) JSONLD {
func NewTestJSONLDManager(t testing.TB) JSONLD {
t.Helper()

contextConfig := DefaultContextConfig()
Expand Down
2 changes: 1 addition & 1 deletion vcr/holder/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.Verifiab

if validateVC {
for _, cred := range credentials {
err := h.verifier.Validate(cred, &options.ProofOptions.Created)
err := h.verifier.VerifySignature(cred, &options.ProofOptions.Created)
if err != nil {
return nil, core.InvalidInputError("invalid credential (id=%s): %w", cred.ID, err)
}
Expand Down
6 changes: 3 additions & 3 deletions vcr/holder/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func TestWallet_BuildPresentation(t *testing.T) {

keyResolver := resolver.NewMockKeyResolver(ctrl)
mockVerifier := verifier.NewMockVerifier(ctrl)
mockVerifier.EXPECT().Validate(testCredential, &created)
mockVerifier.EXPECT().VerifySignature(testCredential, &created)

keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil)

Expand All @@ -240,7 +240,7 @@ func TestWallet_BuildPresentation(t *testing.T) {

keyResolver := resolver.NewMockKeyResolver(ctrl)
mockVerifier := verifier.NewMockVerifier(ctrl)
mockVerifier.EXPECT().Validate(testCredential, &created).Return(errors.New("failed"))
mockVerifier.EXPECT().VerifySignature(testCredential, &created).Return(errors.New("failed"))

keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil)

Expand All @@ -256,7 +256,7 @@ func TestWallet_BuildPresentation(t *testing.T) {

keyResolver := resolver.NewMockKeyResolver(ctrl)
mockVerifier := verifier.NewMockVerifier(ctrl)
mockVerifier.EXPECT().Validate(gomock.Any(), gomock.Any())
mockVerifier.EXPECT().VerifySignature(gomock.Any(), gomock.Any())

keyResolver.EXPECT().ResolveKey(testDID, nil, resolver.NutsSigningKeyType).Return(ssi.MustParseURI(kid), key.Public(), nil)

Expand Down
3 changes: 3 additions & 0 deletions vcr/signature/proof/jsonld.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ func (o ProofOptions) ValidAt(at time.Time, maxSkew time.Duration) bool {
return true
}

var _ Proof = (*LDProof)(nil)
var _ ProofVerifier = (*LDProof)(nil)

// LDProof contains the fields of the Proof data model: https://w3c-ccg.github.io/data-integrity-spec/#proofs
type LDProof struct {
ProofOptions
Expand Down
3 changes: 2 additions & 1 deletion vcr/signature/proof/proof.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package proof

import (
"context"
"crypto"
"encoding/json"
nutsCrypto "github.com/nuts-foundation/nuts-node/crypto"
Expand Down Expand Up @@ -70,7 +71,7 @@ func (d SignedDocument) UnmarshalProofValue(target interface{}) error {
// Proof is the interface that defines a set of methods which a proof should implement.
type Proof interface {
// Sign defines the basic signing operation on the proof.
Sign(document Document, suite signature.Suite, key nutsCrypto.Key) (interface{}, error)
Sign(ctx context.Context, document Document, suite signature.Suite, key nutsCrypto.Key) (interface{}, error)
}

// ProofVerifier defines the generic verifier interface
Expand Down
2 changes: 1 addition & 1 deletion vcr/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (c *vcr) StoreCredential(credential vc.VerifiableCredential, validAt *time.
}

// verify first
if err := c.verifier.Validate(credential, validAt); err != nil {
if err := c.verifier.VerifySignature(credential, validAt); err != nil {
return err
}

Expand Down
8 changes: 4 additions & 4 deletions vcr/verifier/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ import (
// Verifier defines the interface for verifying verifiable credentials.
type Verifier interface {
// Verify checks credential on full correctness. It checks:
// validity of the signature
// validity of the signature (optional)
// if it has been revoked
// if the issuer is registered as trusted
// if the issuer is registered as trusted (optional)
Verify(credential vc.VerifiableCredential, allowUntrusted bool, checkSignature bool, validAt *time.Time) error
// Validate checks the verifiable credential technical correctness
Validate(credentialToVerify vc.VerifiableCredential, at *time.Time) error
// VerifySignature checks that the signature on the verifiable credential is correct and valid at the given time and nothing else
VerifySignature(credentialToVerify vc.VerifiableCredential, at *time.Time) error
// IsRevoked checks if the credential is revoked
IsRevoked(credentialID ssi.URI) (bool, error)
// GetRevocation returns the first revocation by credential ID
Expand Down
24 changes: 12 additions & 12 deletions vcr/verifier/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

128 changes: 128 additions & 0 deletions vcr/verifier/signature_verifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package verifier

import (
crypt "crypto"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/types"
"strings"
"time"

"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/vcr/issuer"
"github.com/nuts-foundation/nuts-node/vcr/signature"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/nuts-foundation/nuts-node/vdr/resolver"
)

type signatureVerifier struct {
keyResolver resolver.KeyResolver
jsonldManager jsonld.JSONLD
}

// VerifySignature implements the Proof Verification Algorithm: https://w3c-ccg.github.io/data-integrity-spec/#proof-verification-algorithm
func (sv *signatureVerifier) VerifySignature(credentialToVerify vc.VerifiableCredential, validateAt *time.Time) error {
switch credentialToVerify.Format() {
case issuer.JSONLDCredentialFormat:
return sv.jsonldProof(credentialToVerify, credentialToVerify.Issuer.String(), validateAt)
case issuer.JWTCredentialFormat:
return sv.jwtSignature(credentialToVerify.Raw(), credentialToVerify.Issuer.String(), validateAt)
default:
return errors.New("unsupported credential proof format")
}
}

// verifyVPSignature implements the Proof Verification Algorithm: https://w3c-ccg.github.io/data-integrity-spec/#proof-verification-algorithm
func (sv *signatureVerifier) verifyVPSignature(presentation vc.VerifiablePresentation, validateAt *time.Time) error {
signerDID, err := credential.PresentationSigner(presentation)
if err != nil {
return toVerificationError(err)
}

switch presentation.Format() {
case issuer.JSONLDPresentationFormat:
return sv.jsonldProof(presentation, signerDID.String(), validateAt)
case issuer.JWTPresentationFormat:
return sv.jwtSignature(presentation.Raw(), signerDID.String(), validateAt)
default:
return errors.New("unsupported presentation proof format")
}
}

// jsonldProof checks the signature on a VC or VP in jsonld format (contains a proof)
func (sv *signatureVerifier) jsonldProof(documentToVerify any, issuer string, at *time.Time) error {
signedDocument, err := proof.NewSignedDocument(documentToVerify)
if err != nil {
return newVerificationError("invalid LD-JSON document: %w", err)
}

ldProof := proof.LDProof{}
if err = signedDocument.UnmarshalProofValue(&ldProof); err != nil {
return newVerificationError("unsupported proof type: %w", err)
}

// for a VP this will not fail
verificationMethod := ldProof.VerificationMethod.String()
verificationMethodIssuer := strings.Split(verificationMethod, "#")[0]
if verificationMethodIssuer == "" || verificationMethodIssuer != issuer {
return errVerificationMethodNotOfIssuer
}

// verify signing time
validAt := time.Now()
if at != nil {
validAt = *at
}
if !ldProof.ValidAt(validAt, maxSkew) {
return toVerificationError(types.ErrPresentationNotValidAtTime)
}

// find key
signingKey, err := sv.keyResolver.ResolveKeyByID(ldProof.VerificationMethod.String(), at, resolver.NutsSigningKeyType)
if err != nil {
return fmt.Errorf("unable to resolve valid signing key: %w", err)
}

// verify signature
err = ldProof.Verify(signedDocument.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: sv.jsonldManager.DocumentLoader()}, signingKey)
if err != nil {
return newVerificationError("invalid signature: %w", err)
}
return nil
}

func (sv *signatureVerifier) jwtSignature(jwtDocumentToVerify string, issuer string, at *time.Time) error {
var keyID string
_, err := crypto.ParseJWT(jwtDocumentToVerify, func(kid string) (crypt.PublicKey, error) {
keyID = kid
return sv.resolveSigningKey(kid, issuer, at)
}, jwt.WithClock(jwt.ClockFunc(func() time.Time {
if at == nil {
return time.Now()
}
return *at
})))
if err != nil {
return fmt.Errorf("unable to validate JWT credential: %w", err)
}
if keyID != "" && strings.Split(keyID, "#")[0] != issuer {
return errVerificationMethodNotOfIssuer
}
return nil
}

func (sv *signatureVerifier) 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 sv.keyResolver.ResolveKeyByID(kid, at, resolver.NutsSigningKeyType)
}
Loading

0 comments on commit 1a0578e

Please sign in to comment.