From de98e50c818401eeeba104fa5f62f241bde735fa Mon Sep 17 00:00:00 2001 From: Filip Burlacu Date: Tue, 1 Aug 2023 21:18:41 -0400 Subject: [PATCH] feat: data-integrity support for ecdsa-2019 suite Signed-off-by: Filip Burlacu --- .../models/dataintegrity/dataintegrity.go | 27 +- .../models/dataintegrity/integration_test.go | 197 ++++++++ .../models/dataintegrity/models/models.go | 39 +- component/models/dataintegrity/signer.go | 39 +- component/models/dataintegrity/signer_test.go | 347 +++++++++----- .../suite/ecdsa2019/ecdsa2019.go | 224 ++++++++- .../suite/ecdsa2019/ecdsa2019_test.go | 428 ++++++++++++++++++ .../suite/ecdsa2019/integration_test.go | 148 ++++++ .../ecdsa2019/testdata/invalid_jsonld.jsonld | 15 + .../testdata/valid_credential.jsonld | 19 + component/models/dataintegrity/suite/suite.go | 3 + .../models/dataintegrity/support_test.go | 49 ++ component/models/dataintegrity/verifier.go | 28 +- .../models/dataintegrity/verifier_test.go | 332 ++++++++++---- component/models/jwt/didsignjwt/signjwt.go | 44 +- 15 files changed, 1704 insertions(+), 235 deletions(-) create mode 100644 component/models/dataintegrity/integration_test.go create mode 100644 component/models/dataintegrity/suite/ecdsa2019/ecdsa2019_test.go create mode 100644 component/models/dataintegrity/suite/ecdsa2019/integration_test.go create mode 100644 component/models/dataintegrity/suite/ecdsa2019/testdata/invalid_jsonld.jsonld create mode 100644 component/models/dataintegrity/suite/ecdsa2019/testdata/valid_credential.jsonld diff --git a/component/models/dataintegrity/dataintegrity.go b/component/models/dataintegrity/dataintegrity.go index c99a41e65..863dc82fd 100644 --- a/component/models/dataintegrity/dataintegrity.go +++ b/component/models/dataintegrity/dataintegrity.go @@ -6,10 +6,31 @@ SPDX-License-Identifier: Apache-2.0 package dataintegrity -import "errors" +import ( + "errors" + + "github.com/hyperledger/aries-framework-go/component/models/did" + spivdr "github.com/hyperledger/aries-framework-go/spi/vdr" +) var ( - // ErrUnsupportedSuite is returned when a Signer or Verifier is required to use a cryptographic suite for which it - // doesn't have a suite.Signer or suite.Verifier (respectively) initialized. + // ErrUnsupportedSuite is returned when a Signer or Verifier is required to use + // a cryptographic suite for which it doesn't have a suite.Signer or + // suite.Verifier (respectively) initialized. ErrUnsupportedSuite = errors.New("data integrity proof requires unsupported cryptographic suite") + // ErrNoResolver is returned when a Signer or Verifier needs to resolve a + // verification method but has no DID resolver. + ErrNoResolver = errors.New("either did resolver or both verification method and verification relationship must be provided") //nolint:lll + // ErrVMResolution is returned when a Signer or Verifier needs to resolve a + // verification method but this fails. + ErrVMResolution = errors.New("failed to resolve verification method") ) + +type didResolver interface { + Resolve(did string, opts ...spivdr.DIDMethodOption) (*did.DocResolution, error) +} + +// Options contains initialization parameters for Data Integrity Signer and Verifier. +type Options struct { + DIDResolver didResolver +} diff --git a/component/models/dataintegrity/integration_test.go b/component/models/dataintegrity/integration_test.go new file mode 100644 index 000000000..d2035c0ec --- /dev/null +++ b/component/models/dataintegrity/integration_test.go @@ -0,0 +1,197 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package dataintegrity + +import ( + _ "embed" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/util/jwkkid" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/kms/localkms" + mockkms "github.com/hyperledger/aries-framework-go/component/kmscrypto/mock/kms" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/secretlock/noop" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/suite/ecdsa2019" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/ld/documentloader" + mockldstore "github.com/hyperledger/aries-framework-go/component/models/ld/mock" + "github.com/hyperledger/aries-framework-go/component/models/ld/store" + mockstorage "github.com/hyperledger/aries-framework-go/component/storageutil/mock/storage" + kmsapi "github.com/hyperledger/aries-framework-go/spi/kms" +) + +var ( + //go:embed suite/ecdsa2019/testdata/valid_credential.jsonld + validCredential []byte +) + +const ( + mockVMID2 = "#key-2" + mockKID2 = mockDID + mockVMID2 +) + +func TestIntegration(t *testing.T) { + signerOpts := suiteOptions(t) + + signerInit := ecdsa2019.NewSigner(signerOpts) + + verifierInit := ecdsa2019.NewVerifier(suiteOptions(t)) + + _, p256Bytes, err := signerOpts.KMS.CreateAndExportPubKeyBytes(kmsapi.ECDSAP256IEEEP1363) + require.NoError(t, err) + + p256JWK, err := jwkkid.BuildJWK(p256Bytes, kmsapi.ECDSAP256IEEEP1363) + require.NoError(t, err) + + _, p384Bytes, err := signerOpts.KMS.CreateAndExportPubKeyBytes(kmsapi.ECDSAP384IEEEP1363) + require.NoError(t, err) + + p384JWK, err := jwkkid.BuildJWK(p384Bytes, kmsapi.ECDSAP384IEEEP1363) + require.NoError(t, err) + + p256VM, err := did.NewVerificationMethodFromJWK(mockVMID, "JsonWebKey2020", mockDID, p256JWK) + require.NoError(t, err) + + p384VM, err := did.NewVerificationMethodFromJWK(mockVMID2, "JsonWebKey2020", mockDID, p384JWK) + require.NoError(t, err) + + resolver := resolveFunc(func(id string) (*did.DocResolution, error) { + switch id { + case mockKID: + return makeMockDIDResolution(id, p256VM, did.AssertionMethod), nil + case mockKID2: + return makeMockDIDResolution(id, p384VM, did.AssertionMethod), nil + } + + return nil, ErrVMResolution + }) + + signer, err := NewSigner(&Options{DIDResolver: resolver}, signerInit) + require.NoError(t, err) + + verifier, err := NewVerifier(&Options{DIDResolver: resolver}, verifierInit) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + t.Run("P-256 key", func(t *testing.T) { + proofOpts := &models.ProofOptions{ + VerificationMethod: p256VM, + VerificationMethodID: p256VM.ID, + SuiteType: ecdsa2019.SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + MaxAge: 100, + } + + signedCred, err := signer.AddProof(validCredential, proofOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(signedCred, proofOpts) + require.NoError(t, err) + }) + + t.Run("P-384 key", func(t *testing.T) { + proofOpts := &models.ProofOptions{ + VerificationMethod: p384VM, + VerificationMethodID: p384VM.ID, + SuiteType: ecdsa2019.SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + MaxAge: 100, + } + + signedCred, err := signer.AddProof(validCredential, proofOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(signedCred, proofOpts) + require.NoError(t, err) + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("wrong key", func(t *testing.T) { + signOpts := &models.ProofOptions{ + VerificationMethod: p256VM, + VerificationMethodID: p256VM.ID, + SuiteType: ecdsa2019.SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + } + + verifyOpts := &models.ProofOptions{ + VerificationMethod: p384VM, + VerificationMethodID: p384VM.ID, + SuiteType: ecdsa2019.SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + MaxAge: 100, + } + + signedCred, err := signer.AddProof(validCredential, signOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(signedCred, verifyOpts) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to verify ecdsa-2019 DI proof") + }) + }) +} + +func suiteOptions(t *testing.T) *ecdsa2019.Options { + t.Helper() + + docLoader, err := documentloader.NewDocumentLoader(createMockProvider()) + require.NoError(t, err) + + storeProv := mockstorage.NewMockStoreProvider() + + kmsProv, err := mockkms.NewProviderForKMS(storeProv, &noop.NoLock{}) + require.NoError(t, err) + + kms, err := localkms.New("local-lock://custom/master/key/", kmsProv) + require.NoError(t, err) + + cr, err := tinkcrypto.New() + require.NoError(t, err) + + return &ecdsa2019.Options{ + LDDocumentLoader: docLoader, + Crypto: cr, + KMS: kms, + } +} + +type provider struct { + ContextStore store.ContextStore + RemoteProviderStore store.RemoteProviderStore +} + +func (p *provider) JSONLDContextStore() store.ContextStore { + return p.ContextStore +} + +func (p *provider) JSONLDRemoteProviderStore() store.RemoteProviderStore { + return p.RemoteProviderStore +} + +func createMockProvider() *provider { + return &provider{ + ContextStore: mockldstore.NewMockContextStore(), + RemoteProviderStore: mockldstore.NewMockRemoteProviderStore(), + } +} diff --git a/component/models/dataintegrity/models/models.go b/component/models/dataintegrity/models/models.go index b548dc99b..2e2b5b318 100644 --- a/component/models/dataintegrity/models/models.go +++ b/component/models/dataintegrity/models/models.go @@ -6,25 +6,28 @@ SPDX-License-Identifier: Apache-2.0 package models -import "time" +import ( + "time" -// TODO integrate VerificationMethod model with the did doc VM model -// (can we just use did.VerificationMethod directly)? + "github.com/hyperledger/aries-framework-go/component/models/did" +) + +const ( + // DataIntegrityProof is the type property on proofs created using data + // integrity cryptographic suites. + DataIntegrityProof = "DataIntegrityProof" +) // VerificationMethod implements the data integrity verification method model: // https://www.w3.org/TR/vc-data-integrity/#verification-methods -type VerificationMethod struct { - ID string `json:"id"` - Type string `json:"type"` - Controller string `json:"controller"` - Fields map[string]interface{} -} +type VerificationMethod = did.VerificationMethod // Proof implements the data integrity proof model: // https://www.w3.org/TR/vc-data-integrity/#proofs type Proof struct { ID string `json:"id,omitempty"` Type string `json:"type"` + CryptoSuite string `json:"cryptosuite,omitempty"` ProofPurpose string `json:"proofPurpose"` VerificationMethod string `json:"verificationMethod"` Created string `json:"created,omitempty"` @@ -36,13 +39,17 @@ type Proof struct { // ProofOptions provides options for signing or verifying a data integrity proof. type ProofOptions struct { - Purpose string - VerificationMethod *VerificationMethod - SuiteType string - Domain string - Challenge string - MaxAge int64 - CustomFields map[string]interface{} + Purpose string + VerificationMethodID string + VerificationMethod *VerificationMethod + VerificationRelationship string + ProofType string + SuiteType string + Domain string + Challenge string + Created time.Time + MaxAge int64 + CustomFields map[string]interface{} } // DateTimeFormat is the date-time format used by the data integrity diff --git a/component/models/dataintegrity/signer.go b/component/models/dataintegrity/signer.go index c5c41de61..dd263135a 100644 --- a/component/models/dataintegrity/signer.go +++ b/component/models/dataintegrity/signer.go @@ -12,6 +12,8 @@ import ( "github.com/tidwall/sjson" + "github.com/hyperledger/aries-framework-go/component/models/jwt/didsignjwt" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/suite" ) @@ -19,14 +21,20 @@ import ( // Signer implements the Add Proof algorithm of the verifiable credential data // integrity specification, using a set of provided cryptographic suites. type Signer struct { - suites map[string]suite.Signer + suites map[string]suite.Signer + resolver didResolver } // NewSigner initializes a Signer that supports using the provided cryptographic // suites to perform data integrity signing. -func NewSigner(suites ...suite.SignerInitializer) (*Signer, error) { +func NewSigner(opts *Options, suites ...suite.SignerInitializer) (*Signer, error) { + if opts == nil { + opts = &Options{} + } + signer := &Signer{ - suites: map[string]suite.Signer{}, + suites: map[string]suite.Signer{}, + resolver: opts.DIDResolver, } for _, initializer := range suites { @@ -67,6 +75,11 @@ func (s *Signer) AddProof(doc []byte, opts *models.ProofOptions) ([]byte, error) return nil, ErrUnsupportedSuite } + err := resolveVM(opts, s.resolver) + if err != nil { + return nil, err + } + proof, err := signerSuite.CreateProof(doc, opts) if err != nil { return nil, ErrProofGeneration @@ -100,3 +113,23 @@ func (s *Signer) AddProof(doc []byte, opts *models.ProofOptions) ([]byte, error) return out, nil } + +func resolveVM(opts *models.ProofOptions, resolver didResolver) error { + if opts.VerificationMethod == nil || opts.VerificationRelationship == "" { + if resolver == nil { + return ErrNoResolver + } + + vm, vmID, rel, err := didsignjwt.ResolveSigningVMWithRelationship(opts.VerificationMethodID, resolver) + if err != nil { + // TODO update linter to use go 1.20: https://github.com/hyperledger/aries-framework-go/issues/3613 + return errors.Join(ErrVMResolution, err) // nolint:typecheck + } + + opts.VerificationMethodID = vmID + opts.VerificationMethod = vm + opts.VerificationRelationship = rel + } + + return nil +} diff --git a/component/models/dataintegrity/signer_test.go b/component/models/dataintegrity/signer_test.go index bf2f9f04d..bbeac3cec 100644 --- a/component/models/dataintegrity/signer_test.go +++ b/component/models/dataintegrity/signer_test.go @@ -15,21 +15,25 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/sjson" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" ) func TestNewSigner(t *testing.T) { t.Run("success", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }, &mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType + "-but-different", - }, &mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }, &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType + "-but-different", + }, &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) require.NotNil(t, s) @@ -37,11 +41,13 @@ func TestNewSigner(t *testing.T) { }) t.Run("initializer error", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - initErr: errExpected, - typeStr: mockSuiteType, - }) + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + initErr: errExpected, + typeStr: mockSuiteType, + }) require.Nil(t, s) require.ErrorIs(t, err, errExpected) @@ -54,32 +60,40 @@ func TestSigner_AddProof(t *testing.T) { t.Run("success", func(t *testing.T) { createdTime := time.Now().Format(models.DateTimeFormat) - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", - Domain: "mock-domain", - Challenge: "mock-challenge", - Created: createdTime, + s, err := NewSigner( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: "#key-1", + }, + vr: did.AssertionMethod, }, }, - typeStr: mockSuiteType, - }) + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + Domain: "mock-domain", + Challenge: "mock-challenge", + Created: createdTime, + }, + }, + typeStr: mockSuiteType, + }) require.NoError(t, err) signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ - SuiteType: mockSuiteType, - Domain: "mock-domain", - Challenge: "mock-challenge", - MaxAge: 1000, + SuiteType: mockSuiteType, + VerificationMethodID: "did:foo:bar#key-1", + Domain: "mock-domain", + Challenge: "mock-challenge", + MaxAge: 1000, }) require.NoError(t, err) - fmt.Println(string(signedDoc)) - expectProof := []byte(fmt.Sprintf(`{ "type": "mock-suite-2023", "proofPurpose": "mock-purpose", @@ -98,16 +112,18 @@ func TestSigner_AddProof(t *testing.T) { t.Run("failure", func(t *testing.T) { t.Run("unsupported suite", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + }, }, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) @@ -116,143 +132,248 @@ func TestSigner_AddProof(t *testing.T) { require.Nil(t, signedDoc) }) - t.Run("suite create proof", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofErr: errExpected, - }, - typeStr: mockSuiteType, - }) + t.Run("no resolver", func(t *testing.T) { + createdTime := time.Now().Format(models.DateTimeFormat) + + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + Domain: "mock-domain", + Challenge: "mock-challenge", + Created: createdTime, + }, + }, + typeStr: mockSuiteType, + }) require.NoError(t, err) - signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{SuiteType: mockSuiteType}) - require.ErrorIs(t, err, ErrProofGeneration) - require.Nil(t, signedDoc) + _, err = s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationRelationship: "assertionMethod", + Domain: "mock-domain", + Challenge: "mock-challenge", + MaxAge: 1000, + }) + require.ErrorIs(t, err, ErrNoResolver) }) - t.Run("missing required field", func(t *testing.T) { - t.Run("type", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ + t.Run("resolver error", func(t *testing.T) { + createdTime := time.Now().Format(models.DateTimeFormat) + + s, err := NewSigner( + &Options{ + DIDResolver: &mockResolver{ + err: errExpected, + }, + }, + &mockSuiteInitializer{ mockSuite: &mockSuite{ CreateProofVal: &models.Proof{ - // Type: mockSuiteType, + Type: mockSuiteType, ProofPurpose: "mock-purpose", VerificationMethod: "mock-vm", + Domain: "mock-domain", + Challenge: "mock-challenge", + Created: createdTime, }, }, typeStr: mockSuiteType, }) + require.NoError(t, err) + + _, err = s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationRelationship: "assertionMethod", + Domain: "mock-domain", + Challenge: "mock-challenge", + MaxAge: 1000, + }) + require.ErrorIs(t, err, errExpected) + require.ErrorIs(t, err, ErrVMResolution) + }) + + t.Run("suite create proof", func(t *testing.T) { + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofErr: errExpected, + }, + typeStr: mockSuiteType, + }) + + require.NoError(t, err) + + signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + SuiteType: mockSuiteType, + }) + require.ErrorIs(t, err, ErrProofGeneration) + require.Nil(t, signedDoc) + }) + + t.Run("missing required field", func(t *testing.T) { + t.Run("type", func(t *testing.T) { + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + // Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + }, + }, + typeStr: mockSuiteType, + }) + require.NoError(t, err) - signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{SuiteType: mockSuiteType}) + signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) }) t.Run("proofPurpose", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - // ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + // ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + }, }, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) - signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{SuiteType: mockSuiteType}) + signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) }) t.Run("verificationMethod", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - // VerificationMethod: "mock-vm", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + // VerificationMethod: "mock-vm", + }, }, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) - signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{SuiteType: mockSuiteType}) + signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) }) t.Run("created, required by suite", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + }, + ReqCreatedVal: true, }, - ReqCreatedVal: true, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) - signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{SuiteType: mockSuiteType}) + signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ + SuiteType: mockSuiteType, + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) }) }) t.Run("incorrect domain", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", - Domain: "wrong-domain", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + Domain: "wrong-domain", + }, }, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ - SuiteType: mockSuiteType, - Domain: "expected-domain", + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", + SuiteType: mockSuiteType, + Domain: "expected-domain", }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) }) t.Run("incorrect challenge", func(t *testing.T) { - s, err := NewSigner(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - CreateProofVal: &models.Proof{ - Type: mockSuiteType, - ProofPurpose: "mock-purpose", - VerificationMethod: "mock-vm", - Domain: "expected-domain", - Challenge: "wrong-challenge", + s, err := NewSigner( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + CreateProofVal: &models.Proof{ + Type: mockSuiteType, + ProofPurpose: "mock-purpose", + VerificationMethod: "mock-vm", + Domain: "expected-domain", + Challenge: "wrong-challenge", + }, }, - }, - typeStr: mockSuiteType, - }) + typeStr: mockSuiteType, + }) require.NoError(t, err) signedDoc, err := s.AddProof(mockDoc, &models.ProofOptions{ - SuiteType: mockSuiteType, - Domain: "expected-domain", - Challenge: "expected-challenge", + SuiteType: mockSuiteType, + Domain: "expected-domain", + Challenge: "expected-challenge", + VerificationMethod: &did.VerificationMethod{}, + VerificationRelationship: "assertionMethod", }) require.ErrorIs(t, err, ErrProofGeneration) require.Nil(t, signedDoc) diff --git a/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019.go b/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019.go index b7a6500d6..bf080592a 100644 --- a/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019.go +++ b/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019.go @@ -7,15 +7,27 @@ SPDX-License-Identifier: Apache-2.0 package ecdsa2019 import ( + "crypto" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/json" "errors" + "fmt" + "hash" + "github.com/multiformats/go-multibase" + "github.com/piprate/json-gold/ld" + + kmsapi "github.com/hyperledger/aries-framework-go/spi/kms" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/suite" + "github.com/hyperledger/aries-framework-go/component/models/ld/processor" + cryptoapi "github.com/hyperledger/aries-framework-go/spi/crypto" ) -// TODO note: a Suite could implement both signer and verifier in the same type, or could have separate implementations -// so, for example, verifier can be initialized without needing permissions/access for private key usage - const ( // SuiteType "ecdsa-2019" is the data integrity Type identifier for the suite // implementing ecdsa signatures with RDF canonicalization as per this @@ -24,10 +36,18 @@ const ( ) // Suite implements the ecdsa-2019 data integrity cryptographic suite. -type Suite struct{} +type Suite struct { + ldLoader ld.DocumentLoader + crypto cryptoapi.Crypto + kms kmsapi.KeyManager +} // Options provides initialization options for Suite. -type Options struct{} +type Options struct { + LDDocumentLoader ld.DocumentLoader + Crypto cryptoapi.Crypto + KMS kmsapi.KeyManager +} // SuiteInitializer is the initializer for Suite. type SuiteInitializer func() (suite.Suite, error) @@ -35,7 +55,11 @@ type SuiteInitializer func() (suite.Suite, error) // New constructs an initializer for Suite. func New(options *Options) SuiteInitializer { return func() (suite.Suite, error) { - return &Suite{}, nil + return &Suite{ + ldLoader: options.LDDocumentLoader, + crypto: options.Crypto, + kms: options.KMS, + }, nil } } @@ -69,16 +93,108 @@ func NewVerifier(options *Options) suite.VerifierInitializer { return initializer(New(options)) } +const ( + ldCtxKey = "@context" +) + // CreateProof implements the ecdsa-2019 cryptographic suite for Add Proof: // https://www.w3.org/TR/vc-di-ecdsa/#add-proof-ecdsa-2019 func (s *Suite) CreateProof(doc []byte, opts *models.ProofOptions) (*models.Proof, error) { - return nil, errors.New("implement me") + docHash, vmKey, err := s.transformAndHash(doc, opts) + if err != nil { + return nil, err + } + + sig, err := sign(docHash, vmKey, s.crypto, s.kms) + if err != nil { + return nil, err + } + + sigStr, err := multibase.Encode(multibase.Base58BTC, sig) + if err != nil { + return nil, err + } + + p := &models.Proof{ + Type: models.DataIntegrityProof, + CryptoSuite: SuiteType, + ProofPurpose: opts.Purpose, + Domain: opts.Domain, + Challenge: opts.Challenge, + VerificationMethod: opts.VerificationMethod.ID, + ProofValue: sigStr, + } + + return p, nil +} + +func (s *Suite) transformAndHash(doc []byte, opts *models.ProofOptions) ([]byte, *jwk.JWK, error) { + docData := make(map[string]interface{}) + + err := json.Unmarshal(doc, &docData) + if err != nil { + return nil, nil, fmt.Errorf("ecdsa-2019 suite expects JSON-LD payload: %w", err) + } + + vmKey := opts.VerificationMethod.JSONWebKey() + if vmKey == nil { + return nil, nil, errors.New("verification method needs JWK") + } + + var h hash.Hash + + switch vmKey.Crv { + case "P-256": + h = sha256.New() + case "P-384": + h = sha512.New384() + default: + return nil, nil, errors.New("unsupported ECDSA curve") + } + + confData, err := proofConfig(docData[ldCtxKey], opts) + if err != nil { + return nil, nil, err + } + + if opts.ProofType != "DataIntegrityProof" || opts.SuiteType != SuiteType { + return nil, nil, suite.ErrProofTransformation + } + + canonDoc, err := canonicalize(docData, s.ldLoader) + if err != nil { + return nil, nil, err + } + + canonConf, err := canonicalize(confData, s.ldLoader) + if err != nil { + return nil, nil, err + } + + docHash := hashData(canonDoc, canonConf, h) + + return docHash, vmKey, nil } // VerifyProof implements the ecdsa-2019 cryptographic suite for Verify Proof: // https://www.w3.org/TR/vc-di-ecdsa/#verify-proof-ecdsa-2019 func (s *Suite) VerifyProof(doc []byte, proof *models.Proof, opts *models.ProofOptions) error { - return errors.New("implement me") + sigBase, vmKey, err := s.transformAndHash(doc, opts) + if err != nil { + return err + } + + _, sig, err := multibase.Decode(proof.ProofValue) + if err != nil { + return fmt.Errorf("decoding proofValue: %w", err) + } + + err = verify(sigBase, sig, vmKey, s.crypto, s.kms) + if err != nil { + return fmt.Errorf("failed to verify ecdsa-2019 DI proof: %w", err) + } + + return nil } // RequiresCreated returns false, as the ecdsa-2019 cryptographic suite does not @@ -86,3 +202,95 @@ func (s *Suite) VerifyProof(doc []byte, proof *models.Proof, opts *models.ProofO func (s *Suite) RequiresCreated() bool { return false } + +func canonicalize(data map[string]interface{}, loader ld.DocumentLoader) ([]byte, error) { + out, err := processor.Default().GetCanonicalDocument(data, processor.WithDocumentLoader(loader)) + if err != nil { + return nil, fmt.Errorf("canonicalizing signature base data: %w", err) + } + + return out, nil +} + +func hashData(transformedDoc, confData []byte, h hash.Hash) []byte { + h.Write(transformedDoc) + docHash := h.Sum(nil) + + h.Reset() + h.Write(confData) + result := h.Sum(docHash) + + return result +} + +func proofConfig(docCtx interface{}, opts *models.ProofOptions) (map[string]interface{}, error) { + if opts.Purpose != opts.VerificationRelationship { + return nil, errors.New("verification method is not suitable for purpose") + } + + timeStr := opts.Created.Format(models.DateTimeFormat) + + conf := map[string]interface{}{ + ldCtxKey: docCtx, + "type": models.DataIntegrityProof, + "cryptosuite": SuiteType, + "verificationMethod": opts.VerificationMethodID, + "created": timeStr, + "proofPurpose": opts.Purpose, + } + + return conf, nil +} + +// TODO copied from kid_creator.go, should move there: https://github.com/hyperledger/aries-framework-go/issues/3614 +func kmsKID(key *jwk.JWK) (string, error) { + tp, err := key.Thumbprint(crypto.SHA256) + if err != nil { + return "", fmt.Errorf("computing thumbprint for kms kid: %w", err) + } + + return base64.RawURLEncoding.EncodeToString(tp), nil +} + +func sign(sigBase []byte, key *jwk.JWK, cr cryptoapi.Crypto, kms kmsapi.KeyManager) ([]byte, error) { + kid, err := kmsKID(key) + if err != nil { + return nil, err + } + + kh, err := kms.Get(kid) + if err != nil { + return nil, err + } + + sig, err := cr.Sign(sigBase, kh) + if err != nil { + return nil, err + } + + return sig, nil +} + +func verify(sigBase, sig []byte, key *jwk.JWK, cr cryptoapi.Crypto, kms kmsapi.KeyManager) error { + pkBytes, err := key.PublicKeyBytes() + if err != nil { + return fmt.Errorf("getting verification key bytes: %w", err) + } + + kt, err := key.KeyType() + if err != nil { + return fmt.Errorf("getting key type of verification key: %w", err) + } + + kh, err := kms.PubKeyBytesToHandle(pkBytes, kt) + if err != nil { + return err + } + + err = cr.Verify(sig, sigBase, kh) + if err != nil { + return err + } + + return nil +} diff --git a/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019_test.go b/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019_test.go new file mode 100644 index 000000000..45c284604 --- /dev/null +++ b/component/models/dataintegrity/suite/ecdsa2019/ecdsa2019_test.go @@ -0,0 +1,428 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdsa2019 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + _ "embed" + "errors" + "testing" + "time" + + "github.com/multiformats/go-multibase" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/jose/jwk/jwksupport" + mockcrypto "github.com/hyperledger/aries-framework-go/component/kmscrypto/mock/crypto" + mockkms "github.com/hyperledger/aries-framework-go/component/kmscrypto/mock/kms" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/suite" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/ld/documentloader" + mockldstore "github.com/hyperledger/aries-framework-go/component/models/ld/mock" + "github.com/hyperledger/aries-framework-go/component/models/ld/store" +) + +var ( + //go:embed testdata/valid_credential.jsonld + validCredential []byte + //go:embed testdata/invalid_jsonld.jsonld + invalidJSONLD []byte +) + +const ( + fooBar = "foo bar" +) + +func TestNew(t *testing.T) { + docLoader, err := documentloader.NewDocumentLoader(createMockProvider()) + require.NoError(t, err) + + cryp := &mockcrypto.Crypto{} + kms := &mockkms.KeyManager{} + + t.Run("signer success", func(t *testing.T) { + sigInit := NewSigner(&Options{ + LDDocumentLoader: docLoader, + Crypto: cryp, + KMS: kms, + }) + + signer, err := sigInit.Signer() + require.NoError(t, err) + require.NotNil(t, signer) + require.False(t, signer.RequiresCreated()) + }) + + t.Run("verifier success", func(t *testing.T) { + verInit := NewVerifier(&Options{ + LDDocumentLoader: docLoader, + Crypto: cryp, + KMS: kms, + }) + + verifier, err := verInit.Verifier() + require.NoError(t, err) + require.NotNil(t, verifier) + require.False(t, verifier.RequiresCreated()) + }) +} + +type testCase struct { + crypto *mockcrypto.Crypto + kms *mockkms.KeyManager + docLoader *documentloader.DocumentLoader + proofOpts *models.ProofOptions + proof *models.Proof + document []byte + errIs error + errStr string +} + +func successCase(t *testing.T) *testCase { + t.Helper() + + _, mockVM := getVMWithJWK(t) + + docLoader, err := documentloader.NewDocumentLoader(createMockProvider()) + require.NoError(t, err) + + cryp := &mockcrypto.Crypto{} + keyManager := &mockkms.KeyManager{} + + proofCreated := time.Now() + + proofOpts := &models.ProofOptions{ + VerificationMethod: mockVM, + VerificationMethodID: mockVM.ID, + SuiteType: SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: proofCreated, + MaxAge: 100, + } + + mockSig, err := multibase.Encode(multibase.Base58BTC, []byte("mock signature")) + require.NoError(t, err) + + proof := &models.Proof{ + Type: models.DataIntegrityProof, + CryptoSuite: SuiteType, + ProofPurpose: "assertionMethod", + VerificationMethod: mockVM.ID, + Created: proofCreated.Format(models.DateTimeFormat), + ProofValue: mockSig, + } + + return &testCase{ + crypto: cryp, + kms: keyManager, + docLoader: docLoader, + proofOpts: proofOpts, + proof: proof, + document: validCredential, + errIs: nil, + errStr: "", + } +} + +func testSign(t *testing.T, tc *testCase) { + sigInit := NewSigner(&Options{ + LDDocumentLoader: tc.docLoader, + Crypto: tc.crypto, + KMS: tc.kms, + }) + + signer, err := sigInit.Signer() + require.NoError(t, err) + + proof, err := signer.CreateProof(tc.document, tc.proofOpts) + + if tc.errStr == "" && tc.errIs == nil { + require.NoError(t, err) + require.NotNil(t, proof) + } else { + require.Error(t, err) + require.Nil(t, proof) + + if tc.errStr != "" { + require.Contains(t, err.Error(), tc.errStr) + } + + if tc.errIs != nil { + require.ErrorIs(t, err, tc.errIs) + } + } +} + +func testVerify(t *testing.T, tc *testCase) { + verInit := NewVerifier(&Options{ + LDDocumentLoader: tc.docLoader, + Crypto: tc.crypto, + KMS: tc.kms, + }) + + verifier, err := verInit.Verifier() + require.NoError(t, err) + + err = verifier.VerifyProof(tc.document, tc.proof, tc.proofOpts) + + if tc.errStr == "" && tc.errIs == nil { + require.NoError(t, err) + } else { + require.Error(t, err) + + if tc.errStr != "" { + require.Contains(t, err.Error(), tc.errStr) + } + + if tc.errIs != nil { + require.ErrorIs(t, err, tc.errIs) + } + } +} + +func TestSuite_CreateProof(t *testing.T) { + t.Run("success", func(t *testing.T) { + t.Run("P-256 key", func(t *testing.T) { + tc := successCase(t) + + testSign(t, tc) + }) + + t.Run("P-384 key", func(t *testing.T) { + tc := successCase(t) + + tc.proofOpts.VerificationMethod = getP384VM(t) + + testSign(t, tc) + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("compute KMS KID", func(t *testing.T) { + tc := successCase(t) + + badKey, vm := getVMWithJWK(t) + + badKey.Key = fooBar + + tc.proofOpts.VerificationMethod = vm + tc.errStr = "computing thumbprint for kms kid" + + testSign(t, tc) + }) + + t.Run("kms key handle error", func(t *testing.T) { + tc := successCase(t) + + errExpected := errors.New("expected error") + + tc.kms.GetKeyErr = errExpected + tc.errIs = errExpected + + testSign(t, tc) + }) + + t.Run("crypto sign error", func(t *testing.T) { + tc := successCase(t) + + errExpected := errors.New("expected error") + + tc.crypto.SignErr = errExpected + tc.errIs = errExpected + + testSign(t, tc) + }) + }) +} + +func TestSuite_VerifyProof(t *testing.T) { + t.Run("success", func(t *testing.T) { + t.Run("P-256 key", func(t *testing.T) { + tc := successCase(t) + + testVerify(t, tc) + }) + + t.Run("P-384 key", func(t *testing.T) { + tc := successCase(t) + + tc.proofOpts.VerificationMethod = getP384VM(t) + + testVerify(t, tc) + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("decode proof signature", func(t *testing.T) { + tc := successCase(t) + + tc.proof.ProofValue = "!%^@^@#%&#%#@" + tc.errStr = "decoding proofValue" + + testVerify(t, tc) + }) + + t.Run("get verification key bytes", func(t *testing.T) { + tc := successCase(t) + + badKey, vm := getVMWithJWK(t) + + badKey.Key = fooBar + + tc.proofOpts.VerificationMethod = vm + tc.errStr = "getting verification key bytes" + + testVerify(t, tc) + }) + + t.Run("get kms key handle", func(t *testing.T) { + tc := successCase(t) + + errExpected := errors.New("expected error") + + tc.kms.PubKeyBytesToHandleErr = errExpected + tc.errIs = errExpected + + testVerify(t, tc) + }) + + t.Run("crypto verify", func(t *testing.T) { + tc := successCase(t) + + errExpected := errors.New("expected error") + + tc.crypto.VerifyErr = errExpected + tc.errIs = errExpected + + testVerify(t, tc) + }) + }) +} + +func TestSharedFailures(t *testing.T) { + t.Run("unmarshal doc", func(t *testing.T) { + tc := successCase(t) + + tc.document = []byte("not JSON!") + tc.errStr = "expects JSON-LD payload" + + testSign(t, tc) + }) + + t.Run("no jwk in vm", func(t *testing.T) { + tc := successCase(t) + + tc.proofOpts.VerificationMethod = &did.VerificationMethod{ + ID: tc.proofOpts.VerificationMethodID, + Value: []byte(fooBar), + } + tc.errStr = "verification method needs JWK" + + testSign(t, tc) + }) + + t.Run("unsupported ECDSA curve", func(t *testing.T) { + tc := successCase(t) + + badKey, vm := getVMWithJWK(t) + + badKey.Crv = fooBar + + tc.proofOpts.VerificationMethod = vm + tc.errStr = "unsupported ECDSA curve" + + testVerify(t, tc) + }) + + t.Run("wrong purpose", func(t *testing.T) { + tc := successCase(t) + + tc.proofOpts.Purpose = fooBar + tc.errStr = "verification method is not suitable for purpose" + + testSign(t, tc) + }) + + t.Run("invalid proof/suite type", func(t *testing.T) { + tc := successCase(t) + + tc.proofOpts.ProofType = fooBar + tc.errIs = suite.ErrProofTransformation + + testSign(t, tc) + + tc.proofOpts.ProofType = models.DataIntegrityProof + tc.proofOpts.SuiteType = fooBar + + testSign(t, tc) + }) + + t.Run("canonicalize doc", func(t *testing.T) { + tc := successCase(t) + + tc.document = invalidJSONLD + tc.errStr = "canonicalizing signature base data" + + testSign(t, tc) + }) +} + +func getVMWithJWK(t *testing.T) (*jwk.JWK, *models.VerificationMethod) { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + jwkPriv, err := jwksupport.JWKFromKey(priv) + require.NoError(t, err) + + mockVM, err := did.NewVerificationMethodFromJWK("#key-1", "JsonWebKey2020", "did:foo:bar", jwkPriv) + require.NoError(t, err) + + return jwkPriv, mockVM +} + +func getP384VM(t *testing.T) *models.VerificationMethod { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(t, err) + + jwkPriv, err := jwksupport.JWKFromKey(priv) + require.NoError(t, err) + + mockVM, err := did.NewVerificationMethodFromJWK("#key-1", "JsonWebKey2020", "did:foo:bar", jwkPriv) + require.NoError(t, err) + + return mockVM +} + +type provider struct { + ContextStore store.ContextStore + RemoteProviderStore store.RemoteProviderStore +} + +func (p *provider) JSONLDContextStore() store.ContextStore { + return p.ContextStore +} + +func (p *provider) JSONLDRemoteProviderStore() store.RemoteProviderStore { + return p.RemoteProviderStore +} + +func createMockProvider() *provider { + return &provider{ + ContextStore: mockldstore.NewMockContextStore(), + RemoteProviderStore: mockldstore.NewMockRemoteProviderStore(), + } +} diff --git a/component/models/dataintegrity/suite/ecdsa2019/integration_test.go b/component/models/dataintegrity/suite/ecdsa2019/integration_test.go new file mode 100644 index 000000000..aee268107 --- /dev/null +++ b/component/models/dataintegrity/suite/ecdsa2019/integration_test.go @@ -0,0 +1,148 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package ecdsa2019 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/component/kmscrypto/crypto/tinkcrypto" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/doc/util/jwkkid" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/kms/localkms" + mockkms "github.com/hyperledger/aries-framework-go/component/kmscrypto/mock/kms" + "github.com/hyperledger/aries-framework-go/component/kmscrypto/secretlock/noop" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/ld/documentloader" + mockstorage "github.com/hyperledger/aries-framework-go/component/storageutil/mock/storage" + kmsapi "github.com/hyperledger/aries-framework-go/spi/kms" +) + +func TestIntegration(t *testing.T) { + docLoader, err := documentloader.NewDocumentLoader(createMockProvider()) + require.NoError(t, err) + + storeProv := mockstorage.NewMockStoreProvider() + + kmsProv, err := mockkms.NewProviderForKMS(storeProv, &noop.NoLock{}) + require.NoError(t, err) + + kms, err := localkms.New("local-lock://custom/master/key/", kmsProv) + require.NoError(t, err) + + cr, err := tinkcrypto.New() + require.NoError(t, err) + + signerInit := NewSigner(&Options{ + LDDocumentLoader: docLoader, + Crypto: cr, + KMS: kms, + }) + + signer, err := signerInit.Signer() + require.NoError(t, err) + + verifierInit := NewVerifier(&Options{ + LDDocumentLoader: docLoader, + Crypto: cr, + KMS: kms, + }) + + verifier, err := verifierInit.Verifier() + require.NoError(t, err) + + _, p256Bytes, err := kms.CreateAndExportPubKeyBytes(kmsapi.ECDSAP256IEEEP1363) + require.NoError(t, err) + + p256JWK, err := jwkkid.BuildJWK(p256Bytes, kmsapi.ECDSAP256IEEEP1363) + require.NoError(t, err) + + _, p384Bytes, err := kms.CreateAndExportPubKeyBytes(kmsapi.ECDSAP384IEEEP1363) + require.NoError(t, err) + + p384JWK, err := jwkkid.BuildJWK(p384Bytes, kmsapi.ECDSAP384IEEEP1363) + require.NoError(t, err) + + p256VM, err := did.NewVerificationMethodFromJWK("#key-1", "JsonWebKey2020", "did:foo:bar", p256JWK) + require.NoError(t, err) + + p384VM, err := did.NewVerificationMethodFromJWK("#key-2", "JsonWebKey2020", "did:foo:bar", p384JWK) + require.NoError(t, err) + + t.Run("success", func(t *testing.T) { + t.Run("P-256 key", func(t *testing.T) { + proofOpts := &models.ProofOptions{ + VerificationMethod: p256VM, + VerificationMethodID: p256VM.ID, + SuiteType: SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + MaxAge: 100, + } + + proof, err := signer.CreateProof(validCredential, proofOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(validCredential, proof, proofOpts) + require.NoError(t, err) + }) + + t.Run("P-384 key", func(t *testing.T) { + proofOpts := &models.ProofOptions{ + VerificationMethod: p384VM, + VerificationMethodID: p384VM.ID, + SuiteType: SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + MaxAge: 100, + } + + proof, err := signer.CreateProof(validCredential, proofOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(validCredential, proof, proofOpts) + require.NoError(t, err) + }) + }) + + t.Run("failure", func(t *testing.T) { + t.Run("wrong key", func(t *testing.T) { + signOpts := &models.ProofOptions{ + VerificationMethod: p256VM, + VerificationMethodID: p256VM.ID, + SuiteType: SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + Created: time.Now(), + } + + verifyOpts := &models.ProofOptions{ + VerificationMethod: p384VM, + VerificationMethodID: p384VM.ID, + SuiteType: SuiteType, + Purpose: "assertionMethod", + VerificationRelationship: "assertionMethod", + ProofType: models.DataIntegrityProof, + MaxAge: 100, + } + + proof, err := signer.CreateProof(validCredential, signOpts) + require.NoError(t, err) + + err = verifier.VerifyProof(validCredential, proof, verifyOpts) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to verify ecdsa-2019 DI proof") + }) + }) +} diff --git a/component/models/dataintegrity/suite/ecdsa2019/testdata/invalid_jsonld.jsonld b/component/models/dataintegrity/suite/ecdsa2019/testdata/invalid_jsonld.jsonld new file mode 100644 index 000000000..1a77980ad --- /dev/null +++ b/component/models/dataintegrity/suite/ecdsa2019/testdata/invalid_jsonld.jsonld @@ -0,0 +1,15 @@ +{ + "@context": 3.1, + "id": "http://example.edu/credentials/1872", + "type": "VerifiableCredential", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21" + }, + "issuer": { + "id": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "name": "Example University", + "image": "data:image/png;base64,iVBOR" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "expirationDate": "2020-01-01T19:23:24Z" +} diff --git a/component/models/dataintegrity/suite/ecdsa2019/testdata/valid_credential.jsonld b/component/models/dataintegrity/suite/ecdsa2019/testdata/valid_credential.jsonld new file mode 100644 index 000000000..6cca7decd --- /dev/null +++ b/component/models/dataintegrity/suite/ecdsa2019/testdata/valid_credential.jsonld @@ -0,0 +1,19 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/jws/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/1872", + "type": "VerifiableCredential", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21" + }, + "issuer": { + "id": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "name": "Example University", + "image": "data:image/png;base64,iVBOR" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "expirationDate": "2020-01-01T19:23:24Z" +} diff --git a/component/models/dataintegrity/suite/suite.go b/component/models/dataintegrity/suite/suite.go index a62a619c3..e9f2773fa 100644 --- a/component/models/dataintegrity/suite/suite.go +++ b/component/models/dataintegrity/suite/suite.go @@ -72,4 +72,7 @@ var ( // ErrInvalidProof is returned by Verifier.VerifyProof when the given proof is // invalid. ErrInvalidProof = errors.New("data integrity proof invalid") + // ErrProofTransformation is returned by Signer.CreateProof and + // Verifier.VerifyProof when proof transformation fails. + ErrProofTransformation = errors.New("error in data integrity proof transformation") ) diff --git a/component/models/dataintegrity/support_test.go b/component/models/dataintegrity/support_test.go index 823a00ba3..6e96863d1 100644 --- a/component/models/dataintegrity/support_test.go +++ b/component/models/dataintegrity/support_test.go @@ -11,8 +11,11 @@ import ( "github.com/tidwall/gjson" + vdrspi "github.com/hyperledger/aries-framework-go/spi/vdr" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/suite" + "github.com/hyperledger/aries-framework-go/component/models/did" ) var errExpected = errors.New("expected error") @@ -135,3 +138,49 @@ func (m *mockSuiteInitializer) Verifier() (suite.Verifier, error) { func (m *mockSuiteInitializer) Type() string { return m.typeStr } + +type resolveFunc func(id string) (*did.DocResolution, error) + +func (f resolveFunc) Resolve(id string, opts ...vdrspi.DIDMethodOption) (*did.DocResolution, error) { + return f(id) +} + +type mockResolver struct { + vm *did.VerificationMethod + vr did.VerificationRelationship + err error +} + +var _ didResolver = mockResolver{} + +func (m mockResolver) Resolve(id string, opts ...vdrspi.DIDMethodOption) (*did.DocResolution, error) { + if m.err != nil { + return nil, m.err + } + + return makeMockDIDResolution(id, m.vm, m.vr), nil +} + +func makeMockDIDResolution(id string, vm *did.VerificationMethod, vr did.VerificationRelationship) *did.DocResolution { + ver := []did.Verification{{ + VerificationMethod: *vm, + Relationship: vr, + }} + + doc := &did.Doc{ + ID: id, + } + + switch vr { + case did.VerificationRelationshipGeneral: + doc.VerificationMethod = []did.VerificationMethod{*vm} + case did.Authentication: + doc.Authentication = ver + case did.AssertionMethod: + doc.AssertionMethod = ver + } + + return &did.DocResolution{ + DIDDocument: doc, + } +} diff --git a/component/models/dataintegrity/verifier.go b/component/models/dataintegrity/verifier.go index 3d8094413..98fe014ee 100644 --- a/component/models/dataintegrity/verifier.go +++ b/component/models/dataintegrity/verifier.go @@ -25,14 +25,20 @@ const ( // Verifier implements the Verify Proof algorithm of the verifiable credential // data integrity specification, using a set of provided cryptographic suites. type Verifier struct { - suites map[string]suite.Verifier + suites map[string]suite.Verifier + resolver didResolver } // NewVerifier initializes a Verifier that supports using the provided // cryptographic suites to perform data integrity verification. -func NewVerifier(suites ...suite.VerifierInitializer) (*Verifier, error) { +func NewVerifier(opts *Options, suites ...suite.VerifierInitializer) (*Verifier, error) { + if opts == nil { + opts = &Options{} + } + verifier := &Verifier{ - suites: map[string]suite.Verifier{}, + suites: map[string]suite.Verifier{}, + resolver: opts.DIDResolver, } for _, initializer := range suites { @@ -61,6 +67,9 @@ var ( // with a proof that isn't a JSON object or is missing necessary standard // fields. ErrMalformedProof = errors.New("malformed data integrity proof") + // ErrWrongProofType is returned when Verifier.VerifyProof() is given a document + // with a proof that isn't a Data Integrity proof. + ErrWrongProofType = errors.New("proof provided is not a data integrity proof") // ErrMismatchedPurpose is returned when Verifier.VerifyProof() is given a // document with a proof whose Purpose does not match the expected purpose // provided in the proof options. @@ -97,7 +106,11 @@ func (v *Verifier) VerifyProof(doc []byte, opts *models.ProofOptions) error { // return ErrMalformedProof } - verifierSuite, ok := v.suites[proof.Type] + if proof.Type != models.DataIntegrityProof { + return ErrWrongProofType + } + + verifierSuite, ok := v.suites[proof.CryptoSuite] if !ok { return ErrUnsupportedSuite } @@ -115,6 +128,11 @@ func (v *Verifier) VerifyProof(doc []byte, opts *models.ProofOptions) error { // return ErrMalformedProof } + err = resolveVM(opts, v.resolver) + if err != nil { + return err + } + verifyResult := verifierSuite.VerifyProof(unsecuredDoc, proof, opts) if proof.Created != "" { @@ -127,8 +145,6 @@ func (v *Verifier) VerifyProof(doc []byte, opts *models.ProofOptions) error { // diff := now.Sub(createdTime) - // TODO: what should we do if clock skew means the verifier thinks the proof is from the future - if diff > time.Second*time.Duration(opts.MaxAge) { return ErrOutOfDate } diff --git a/component/models/dataintegrity/verifier_test.go b/component/models/dataintegrity/verifier_test.go index 7c9fbf392..59dca0f0c 100644 --- a/component/models/dataintegrity/verifier_test.go +++ b/component/models/dataintegrity/verifier_test.go @@ -14,21 +14,31 @@ import ( "github.com/stretchr/testify/require" "github.com/tidwall/sjson" + "github.com/hyperledger/aries-framework-go/component/models/did" + "github.com/hyperledger/aries-framework-go/component/models/dataintegrity/models" ) +const ( + mockVMID = "#key-1" + mockDID = "did:foo:bar" + mockKID = mockDID + mockVMID +) + func TestNewVerifier(t *testing.T) { t.Run("success", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }, &mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType + "-but-different", - }, &mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }, &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType + "-but-different", + }, &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) require.NotNil(t, v) @@ -36,11 +46,13 @@ func TestNewVerifier(t *testing.T) { }) t.Run("initializer error", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - initErr: errExpected, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + initErr: errExpected, + typeStr: mockSuiteType, + }) require.Nil(t, v) require.ErrorIs(t, err, errExpected) @@ -53,18 +65,28 @@ func TestVerifier_VerifyProof(t *testing.T) { t.Run("success", func(t *testing.T) { createdTime := time.Now().Format(models.DateTimeFormat) - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - ReqCreatedVal: true, + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, }, - typeStr: mockSuiteType, - }) + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + ReqCreatedVal: true, + }, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", Created: createdTime, Domain: "mock-domain", @@ -85,10 +107,12 @@ func TestVerifier_VerifyProof(t *testing.T) { t.Run("failure", func(t *testing.T) { t.Run("missing proof", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) @@ -99,10 +123,12 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("proof json is invalid", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) @@ -116,12 +142,14 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("missing required field", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - ReqCreatedVal: true, - }, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + ReqCreatedVal: true, + }, + typeStr: mockSuiteType, + }) require.NoError(t, err) @@ -142,7 +170,7 @@ func TestVerifier_VerifyProof(t *testing.T) { t.Run("verificationMethod", func(t *testing.T) { mockProof := &models.Proof{ - Type: mockSuiteType, + Type: models.DataIntegrityProof, ProofPurpose: "mock-purpose", } @@ -157,7 +185,7 @@ func TestVerifier_VerifyProof(t *testing.T) { t.Run("proofPurpose", func(t *testing.T) { mockProof := &models.Proof{ - Type: mockSuiteType, + Type: models.DataIntegrityProof, VerificationMethod: "mock-vm", } @@ -172,7 +200,8 @@ func TestVerifier_VerifyProof(t *testing.T) { t.Run("created, with suite that requires it", func(t *testing.T) { mockProof := &models.Proof{ - Type: mockSuiteType, + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, VerificationMethod: "mock-vm", ProofPurpose: "mock-purpose", } @@ -187,16 +216,45 @@ func TestVerifier_VerifyProof(t *testing.T) { }) }) - t.Run("unsupported suite", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, + t.Run("not data integrity proof", func(t *testing.T) { + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }, + ) + + require.NoError(t, err) + + mockProof := &models.Proof{ + Type: "unknown proof type", + VerificationMethod: "mock-vm", + ProofPurpose: "mock-purpose", + } + + signedDoc, err := mockAddProof(mockDoc, mockProof) + require.NoError(t, err) + + err = v.VerifyProof(signedDoc, &models.ProofOptions{ + Purpose: "mock-purpose", }) + require.ErrorIs(t, err, ErrWrongProofType) + }) + + t.Run("unsupported suite", func(t *testing.T) { + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: "unknown-suite", + Type: models.DataIntegrityProof, + CryptoSuite: "unknown-suite", VerificationMethod: "mock-vm", ProofPurpose: "mock-purpose", } @@ -211,15 +269,18 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("mismatched purpose", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, VerificationMethod: "mock-vm", ProofPurpose: "mock-purpose", } @@ -233,19 +294,90 @@ func TestVerifier_VerifyProof(t *testing.T) { require.ErrorIs(t, err, ErrMismatchedPurpose) }) - t.Run("suite verification", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{ - VerifyProofErr: errExpected, + t.Run("no resolver", func(t *testing.T) { + v, err := NewVerifier( + &Options{}, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) + + require.NoError(t, err) + + mockProof := &models.Proof{ + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, + ProofPurpose: "mock-purpose", + } + + signedDoc, err := mockAddProof(mockDoc, mockProof) + require.NoError(t, err) + + err = v.VerifyProof(signedDoc, &models.ProofOptions{ + Purpose: "mock-purpose", + MaxAge: 1000, + }) + require.Error(t, err) + require.ErrorIs(t, err, ErrNoResolver) + }) + + t.Run("resolve error", func(t *testing.T) { + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + err: errExpected, + }, }, - typeStr: mockSuiteType, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) + + require.NoError(t, err) + + mockProof := &models.Proof{ + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, + ProofPurpose: "mock-purpose", + } + + signedDoc, err := mockAddProof(mockDoc, mockProof) + require.NoError(t, err) + + err = v.VerifyProof(signedDoc, &models.ProofOptions{ + Purpose: "mock-purpose", + MaxAge: 1000, }) + require.Error(t, err) + require.ErrorIs(t, err, errExpected) + require.ErrorIs(t, err, ErrVMResolution) + }) + + t.Run("suite verification", func(t *testing.T) { + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, + }, + &mockSuiteInitializer{ + mockSuite: &mockSuite{ + VerifyProofErr: errExpected, + }, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", } @@ -259,16 +391,26 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("created time in wrong format", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, + }, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", Created: "Id. Mar. DCCX AUC", } @@ -283,18 +425,28 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("out of date", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, + }, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) createdTime := time.Now().Add(time.Duration(-50) * time.Second).Format(models.DateTimeFormat) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", Created: createdTime, } @@ -310,16 +462,26 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("proof has wrong domain", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, + }, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", Domain: "wrong-domain", } @@ -335,16 +497,26 @@ func TestVerifier_VerifyProof(t *testing.T) { }) t.Run("proof has wrong challenge", func(t *testing.T) { - v, err := NewVerifier(&mockSuiteInitializer{ - mockSuite: &mockSuite{}, - typeStr: mockSuiteType, - }) + v, err := NewVerifier( + &Options{ + DIDResolver: &mockResolver{ + vm: &did.VerificationMethod{ + ID: mockVMID, + }, + vr: did.AssertionMethod, + }, + }, + &mockSuiteInitializer{ + mockSuite: &mockSuite{}, + typeStr: mockSuiteType, + }) require.NoError(t, err) mockProof := &models.Proof{ - Type: mockSuiteType, - VerificationMethod: "mock-vm", + Type: models.DataIntegrityProof, + CryptoSuite: mockSuiteType, + VerificationMethod: mockKID, ProofPurpose: "mock-purpose", Challenge: "wrong-challenge", } diff --git a/component/models/jwt/didsignjwt/signjwt.go b/component/models/jwt/didsignjwt/signjwt.go index 05dc575da..75c10ebed 100644 --- a/component/models/jwt/didsignjwt/signjwt.go +++ b/component/models/jwt/didsignjwt/signjwt.go @@ -173,17 +173,35 @@ func VerifyJWT(compactJWT string, // - a verification method suitable for signing. // - the full DID#KID identifier of the returned verification method. func ResolveSigningVM(kid string, didResolver didResolver) (*did.VerificationMethod, string, error) { + vm, vmID, _, err := ResolveSigningVMWithRelationship(kid, didResolver) + + return vm, vmID, err +} + +// ResolveSigningVMWithRelationship resolves a DID KeyID using the given did resolver, and returns either: +// +// - the Verification Method identified by the given key ID, or +// - the first Assertion Method in the DID doc, if the DID provided has no fragment component. +// +// Returns: +// - a verification method suitable for signing. +// - the full DID#KID identifier of the returned verification method. +// - the name of the signing-supporting verification relationship found for this verification method. +func ResolveSigningVMWithRelationship( + kid string, + didResolver didResolver, +) (*did.VerificationMethod, string, string, error) { vmSplit := strings.Split(kid, "#") if len(vmSplit) > vmSectionCount { - return nil, "", errors.New("invalid verification method format") + return nil, "", "", errors.New("invalid verification method format") } signingDID := vmSplit[0] docRes, err := didResolver.Resolve(signingDID) if err != nil { - return nil, "", fmt.Errorf("failed to resolve signing DID: %w", err) + return nil, "", "", fmt.Errorf("failed to resolve signing DID: %w", err) } if len(vmSplit) == 1 { @@ -193,10 +211,10 @@ func ResolveSigningVM(kid string, didResolver didResolver) (*did.VerificationMet if len(verificationMethods[did.AssertionMethod]) > 0 { vm := verificationMethods[did.AssertionMethod][0].VerificationMethod - return &vm, fullVMID(signingDID, vm.ID), nil + return &vm, fullVMID(signingDID, vm.ID), "assertionMethod", nil } - return nil, "", fmt.Errorf("DID provided has no assertion method to use as a default signing key") + return nil, "", "", fmt.Errorf("DID provided has no assertion method to use as a default signing key") } vmID := vmSplit[vmSectionCount-1] @@ -205,12 +223,13 @@ func ResolveSigningVM(kid string, didResolver didResolver) (*did.VerificationMet for _, verification := range verifications { if isSigningKey(verification.Relationship) && vmIDFragmentOnly(verification.VerificationMethod.ID) == vmID { vm := verification.VerificationMethod - return &vm, kid, nil + + return &vm, kid, verificationRelationshipName(verification.Relationship), nil } } } - return nil, "", fmt.Errorf("did document has no verification method with given ID") + return nil, "", "", fmt.Errorf("did document has no verification method with given ID") } func fullVMID(did, vmID string) string { @@ -225,6 +244,19 @@ func fullVMID(did, vmID string) string { return vmID } +func verificationRelationshipName(rel did.VerificationRelationship) string { + switch rel { + case did.VerificationRelationshipGeneral: + return "" + case did.AssertionMethod: + return "assertionMethod" + case did.Authentication: + return "authentication" + } + + return "" +} + func vmIDFragmentOnly(vmID string) string { vmSplit := strings.Split(vmID, "#") if len(vmSplit) == 1 {