diff --git a/pkg/vc/command.go b/pkg/vc/command.go index 46d8b4b..2068b66 100644 --- a/pkg/vc/command.go +++ b/pkg/vc/command.go @@ -100,6 +100,27 @@ func (f *Framework) SignPresentation(presentation []byte, privKey []byte, opts * return pres.MarshalJSON() } +// AuthenticateDID creates a verifiable presentation for DID authentication +// https://w3c-ccg.github.io/vp-request-spec/#did-authentication +func (f *Framework) AuthenticateDID(privKey []byte, opts *ProofOptions) ([]byte, error) { + presentation, err := verifiable.NewPresentation() + if err != nil { + return nil, fmt.Errorf("failed to create new presentation for DID authentication: %w", err) + } + + if len(opts.Controller) == 0 { + return nil, fmt.Errorf("controller cannot be empty") + } + + presentation.Holder = opts.Controller + + if err := f.addProof(presentation, privKey, opts); err != nil { + return nil, fmt.Errorf("failed to add proof to DID authentication: %w", err) + } + + return presentation.MarshalJSON() +} + // VerifyPresentation verifies a proof in the verifiable presentation. // If there is a presentation definition provided, also verifies that the presentation meets the requirements. func (f *Framework) VerifyPresentation(vp []byte, opts ...VerificationOption) (*verifiable.Presentation, error) { @@ -204,6 +225,7 @@ type provable interface { // ProofOptions is model to allow the dynamic proofing options by the user. type ProofOptions struct { + Controller string `json:"controller,omitempty"` VerificationMethod string `json:"verificationMethod,omitempty"` SignatureType string `json:"signatureType,omitempty"` ProofPurpose string `json:"proofPurpose,omitempty"` diff --git a/pkg/vc/command_test.go b/pkg/vc/command_test.go index 8f84cdc..d3fa367 100644 --- a/pkg/vc/command_test.go +++ b/pkg/vc/command_test.go @@ -4,15 +4,44 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "testing" + "github.com/hyperledger/aries-framework-go/pkg/doc/did" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" - "testing" + didtypes "github.com/medibloc/panacea-core/v2/x/did/types" "github.com/btcsuite/btcd/btcec" "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" "github.com/stretchr/testify/require" ) +func TestDIDAuthentication_Success(t *testing.T) { + holderPrivKey, err := btcec.NewPrivateKey(btcec.S256()) + require.NoError(t, err) + + mockVDR := NewMockVDR(holderPrivKey.PubKey().SerializeUncompressed(), "EcdsaSecp256k1VerificationKey2019") + f, err := NewFramework(mockVDR) + require.NoError(t, err) + + holderDID := didtypes.NewDID(holderPrivKey.PubKey().SerializeCompressed()) + + proofOpts := &ProofOptions{ + Controller: holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", holderDID), + SignatureType: "EcdsaSecp256k1Signature2019", + Domain: "https://my-domain.com", + Challenge: "this is a challenge", + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuth, err := f.AuthenticateDID(holderPrivKey.Serialize(), proofOpts) + require.NoError(t, err) + + _, err = f.VerifyPresentation(didAuth) + require.NoError(t, err) +} + func TestFullScenarioWithSecp256k1(t *testing.T) { cred := `{"@context": ["https://www.w3.org/2018/credentials/v1","https://www.w3.org/2018/credentials/examples/v1"], "issuer": "did:panacea:BFbUAkxqj3cXXYdNK9FAF9UuEmm7jCT5T77rXhBCvy2K", diff --git a/pkg/vdr/did_auth_test.go b/pkg/vdr/did_auth_test.go new file mode 100644 index 0000000..d8a219d --- /dev/null +++ b/pkg/vdr/did_auth_test.go @@ -0,0 +1,155 @@ +package vdr + +import ( + "fmt" + "testing" + + didtypes "github.com/medibloc/panacea-core/v2/x/did/types" + "github.com/medibloc/vc-sdk/pkg/vc" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/suite" + "github.com/tendermint/tendermint/crypto/secp256k1" +) + +type panaceaDIDAuthTestSuite struct { + suite.Suite + + holderPrivKey secp256k1.PrivKey + attackerPrivKey secp256k1.PrivKey + + holderDID string + attackerDID string + + challenge string + domain string + + VDR *mockDIDClient +} + +func TestPanaceaDIDAuthTestSuite(t *testing.T) { + suite.Run(t, &panaceaDIDAuthTestSuite{}) +} + +func (suite *panaceaDIDAuthTestSuite) BeforeTest(_, _ string) { + holderMnemonic, _ := newMnemonic() + attackerMnemonic, _ := newMnemonic() + + suite.holderPrivKey, _ = generatePrivateKeyFromMnemonic(holderMnemonic) + suite.attackerPrivKey, _ = generatePrivateKeyFromMnemonic(attackerMnemonic) + + suite.holderDID = didtypes.NewDID(suite.holderPrivKey.PubKey().Bytes()) + suite.attackerDID = didtypes.NewDID(suite.attackerPrivKey.PubKey().Bytes()) + + holderPubKeyBase58 := base58.Encode(suite.holderPrivKey.PubKey().Bytes()) + attackerPubKeyBase58 := base58.Encode(suite.attackerPrivKey.PubKey().Bytes()) + + holderDIDDoc := createDIDDoc(suite.holderDID, holderPubKeyBase58) + attackerDIDDoc := createDIDDoc(suite.attackerDID, attackerPubKeyBase58) + + suite.VDR = newMockDIDClient(holderDIDDoc, attackerDIDDoc) + + suite.challenge = "this is a challenge" + suite.domain = "https://my-domain.com" +} + +func (suite *panaceaDIDAuthTestSuite) TestDIDAuthentication_Success() { + panaceaVDR := NewPanaceaVDR(suite.VDR) + f, err := vc.NewFramework(panaceaVDR) + suite.NoError(err) + + proofOpts := &vc.ProofOptions{ + Controller: suite.holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", suite.holderDID), + SignatureType: ecdsaSigType, + Domain: suite.domain, + Challenge: suite.challenge, + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuth, err := f.AuthenticateDID(suite.holderPrivKey.Bytes(), proofOpts) + suite.NoError(err) + + _, err = f.VerifyPresentation(didAuth) + suite.NoError(err) +} + +func (suite *panaceaDIDAuthTestSuite) TestDIDAuthentication_FailNotHolder() { + panaceaVDR := NewPanaceaVDR(suite.VDR) + f, err := vc.NewFramework(panaceaVDR) + suite.NoError(err) + + proofOpts := &vc.ProofOptions{ + Controller: suite.holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", suite.holderDID), + SignatureType: ecdsaSigType, + Domain: suite.domain, + Challenge: suite.challenge, + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuth, err := f.AuthenticateDID(suite.attackerPrivKey.Bytes(), proofOpts) + suite.NoError(err) + + _, err = f.VerifyPresentation(didAuth) + suite.ErrorContains(err, "ecdsa: invalid signature") +} + +func (suite *panaceaDIDAuthTestSuite) TestDIDAuthentication_DifferentChallengeAndDomain() { + panaceaVDR := NewPanaceaVDR(suite.VDR) + f, err := vc.NewFramework(panaceaVDR) + suite.NoError(err) + + proofOpts := &vc.ProofOptions{ + Controller: suite.holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", suite.holderDID), + SignatureType: ecdsaSigType, + Domain: suite.domain, + Challenge: suite.challenge, + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuth, err := f.AuthenticateDID(suite.holderPrivKey.Bytes(), proofOpts) + suite.NoError(err) + + pres, err := f.VerifyPresentation(didAuth) + suite.NoError(err) + + tamperedChallengeProofOpts := &vc.ProofOptions{ + Controller: suite.holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", suite.holderDID), + SignatureType: ecdsaSigType, + Domain: suite.domain, + Challenge: "tampered challenge", + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuthWrongChallenge, err := f.AuthenticateDID(suite.holderPrivKey.Bytes(), tamperedChallengeProofOpts) + suite.NoError(err) + + presWrongChallenge, err := f.VerifyPresentation(didAuthWrongChallenge) + suite.NoError(err) + + tamperedDomainProofOpts := &vc.ProofOptions{ + Controller: suite.holderDID, + VerificationMethod: fmt.Sprintf("%s#key1", suite.holderDID), + SignatureType: ecdsaSigType, + Domain: "tampered domain", + Challenge: suite.challenge, + Created: "2017-06-18T21:19:10Z", + ProofPurpose: "authentication", + } + + didAuthWrongDomain, err := f.AuthenticateDID(suite.holderPrivKey.Bytes(), tamperedDomainProofOpts) + suite.NoError(err) + + presWrongDomain, err := f.VerifyPresentation(didAuthWrongDomain) + suite.NoError(err) + + // compare proofs + suite.NotEqual(pres.Proofs, presWrongChallenge.Proofs) + suite.NotEqual(pres.Proofs, presWrongDomain.Proofs) +}