Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split signature verification from verifier #2683

Merged
merged 2 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 checks if the signature on a VP is valid at a given time
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 checks if the signature on a VP is valid at a given time
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 implements the Proof Verification Algorithm: https://w3c-ccg.github.io/data-integrity-spec/#proof-verification-algorithm
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized clock skew is missing here..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deferred to #2684

if at == nil {
return time.Now()
}
return *at
})))
if err != nil {
return fmt.Errorf("unable to validate JWT signature: %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
Loading