Skip to content

Commit

Permalink
Merge pull request #23 from axone-protocol/feat/create-vc
Browse files Browse the repository at this point in the history
🔐 Create credentials
  • Loading branch information
bdeneux authored Sep 6, 2024
2 parents a8caad1 + 395bf83 commit 713c68e
Show file tree
Hide file tree
Showing 26 changed files with 1,257 additions and 40 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ mock: ## Generate all the mocks (for tests)
@mockgen -source=credential/parser.go -package testutil -destination testutil/credential_mocks.go
@mockgen -package testutil -destination testutil/dataverse_client_mocks.go -mock_names QueryClient=MockDataverseQueryClient github.com/axone-protocol/axone-contract-schema/go/dataverse-schema/v5 QueryClient
@mockgen -package testutil -destination testutil/cognitarium_client_mocks.go -mock_names QueryClient=MockCognitariumQueryClient github.com/axone-protocol/axone-contract-schema/go/cognitarium-schema/v5 QueryClient
@mockgen -package testutil -destination testutil/signer_mocks.go github.com/hyperledger/aries-framework-go/pkg/doc/verifiable Signer
@mockgen -source=credential/generate.go -package testutil -destination testutil/generate_mocks.go

## Help:
.PHONY: help
Expand Down
4 changes: 2 additions & 2 deletions credential/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ func (ac *AuthClaim) From(vc *verifiable.Credential) error {
var _ Parser[*AuthClaim] = (*AuthParser)(nil)

type AuthParser struct {
*credentialParser
*DefaultParser
}

func NewAuthParser(documentLoader ld.DocumentLoader) *AuthParser {
return &AuthParser{
credentialParser: &credentialParser{documentLoader: documentLoader},
DefaultParser: &DefaultParser{documentLoader: documentLoader},
}
}

Expand Down
38 changes: 2 additions & 36 deletions credential/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,10 @@ import (
"testing"

"github.com/axone-protocol/axone-sdk/credential"
jld "github.com/hyperledger/aries-framework-go/pkg/doc/ld"
"github.com/hyperledger/aries-framework-go/pkg/doc/ldcontext"
mockldstore "github.com/hyperledger/aries-framework-go/pkg/mock/ld"
mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider"
"github.com/axone-protocol/axone-sdk/testutil"
. "github.com/smartystreets/goconvey/convey"
)

var (
//go:embed testdata/contexts/credentials-v1.jsonld
mockCredentialsV1JSONLD []byte
//go:embed testdata/contexts/authentication-v4.jsonld
mockAuthenticationV4JSONLD []byte
//go:embed testdata/contexts/security-v2.jsonld
mockSecurityV2JSONLD []byte
)

func TestAuthParser_ParseSigned(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -114,20 +102,7 @@ func TestAuthParser_ParseSigned(t *testing.T) {
raw, err := os.ReadFile(test.file)
So(err, ShouldBeNil)

docLoader, err := jld.NewDocumentLoader(createMockCtxProvider(), jld.WithExtraContexts(
ldcontext.Document{
URL: "https://w3id.org/axone/ontology/v4/schema/credential/digital-service/authentication/",
Content: mockAuthenticationV4JSONLD,
},
ldcontext.Document{
URL: "https://www.w3.org/2018/credentials/v1",
Content: mockCredentialsV1JSONLD,
},
ldcontext.Document{
URL: "https://w3id.org/security/v2",
Content: mockSecurityV2JSONLD,
},
))
docLoader, err := testutil.MockDocumentLoader()
So(err, ShouldBeNil)

parser := credential.NewAuthParser(docLoader)
Expand All @@ -149,12 +124,3 @@ func TestAuthParser_ParseSigned(t *testing.T) {
})
}
}

func createMockCtxProvider() *mockprovider.Provider {
p := &mockprovider.Provider{
ContextStoreValue: mockldstore.NewMockContextStore(),
RemoteProviderStoreValue: mockldstore.NewMockRemoteProviderStore(),
}

return p
}
4 changes: 4 additions & 0 deletions credential/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const (
ErrExtractClaim MessageError = "failed to extract claim"
ErrParse MessageError = "failed to parse verifiable credential"
ErrMalformed MessageError = "malformed verifiable credential"

ErrSign MessageError = "failed to sign verifiable credential"
ErrNoParser MessageError = "no parser provided"
ErrGenerate MessageError = "failed to Generate verifiable credential"
)

type VCError struct {
Expand Down
95 changes: 95 additions & 0 deletions credential/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package credential

import (
"bytes"
"time"

"github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld"
"github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite"
"github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ecdsasecp256k1signature2019"
"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
)

// Generator is a verifiable credential generator.
type Generator struct {
vc Descriptor
signer verifiable.Signer
signerDID string
parser *DefaultParser
}

// New allow to Generate a verifiable credential with the given credential descriptor.
// Example:
//
// vc, err := credential.New(
// template.NewGovernance(
// "datasetID",
// "addr",
// WithID[*GovernanceDescriptor]("id")
// ),
// WithParser(parser),
// WithSignature(signer, "did:key:...")). // Signature is optional and Generate a not signed VC if not provided.
// Generate()
func New(descriptor Descriptor, opts ...Option) *Generator {
g := &Generator{
vc: descriptor,
}
for _, opt := range opts {
opt(g)
}
return g
}

// Option is a function that configures a Generator.
type Option func(*Generator)

func WithParser(parser *DefaultParser) Option {
return func(g *Generator) {
g.parser = parser
}
}

func WithSigner(signer verifiable.Signer, did string) Option {
return func(g *Generator) {
g.signer = signer
g.signerDID = did
}
}

func (generator *Generator) Generate() (*verifiable.Credential, error) {
raw, err := generator.vc.Generate()
if err != nil {
return nil, NewVCError(ErrGenerate, err)
}

if generator.parser == nil {
return nil, NewVCError(ErrNoParser, nil)
}
cred, err := generator.parser.parse(raw.Bytes())
if err != nil {
return nil, NewVCError(ErrParse, err)
}

if generator.signer != nil {
err := cred.AddLinkedDataProof(&verifiable.LinkedDataProofContext{
Created: generator.vc.IssuedAt(),
SignatureType: "EcdsaSecp256k1Signature2019",
Suite: ecdsasecp256k1signature2019.New(suite.WithSigner(generator.signer)),
SignatureRepresentation: verifiable.SignatureJWS,
VerificationMethod: generator.signerDID,
Purpose: generator.vc.ProofPurpose(),
}, jsonld.WithDocumentLoader(generator.parser.documentLoader))
if err != nil {
return nil, NewVCError(ErrSign, err)
}
}

return cred, nil
}

// Descriptor is an interface representing the description of a verifiable credential.
type Descriptor interface {
IssuedAt() *time.Time
Generate() (*bytes.Buffer, error)
ProofPurpose() string
}
116 changes: 116 additions & 0 deletions credential/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//nolint:lll
package credential_test

import (
"bytes"
"errors"
"testing"

"github.com/axone-protocol/axone-sdk/credential"
"github.com/axone-protocol/axone-sdk/testutil"
. "github.com/smartystreets/goconvey/convey"
"go.uber.org/mock/gomock"
)

func TestGenerator_Generate(t *testing.T) {
t.Run("without parser", func(t *testing.T) {
Convey("Given a credential generator with mocked descriptor", t, func() {
controller := gomock.NewController(t)
defer controller.Finish()

mockDescriptor := testutil.NewMockDescriptor(controller)
mockDescriptor.EXPECT().Generate().Return(nil, nil).Times(1)
generator := credential.New(mockDescriptor)

Convey("When generating a credential", func() {
vc, err := generator.Generate()
Convey("Then an error should be returned", func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, "no parser provided")
So(vc, ShouldBeNil)
})
})
})
})

t.Run("error generate", func(t *testing.T) {
Convey("Given a credential generator with mocked descriptor", t, func() {
controller := gomock.NewController(t)
defer controller.Finish()

mockDescriptor := testutil.NewMockDescriptor(controller)
mockDescriptor.EXPECT().Generate().Return(nil, errors.New("failed")).Times(1)
generator := credential.New(mockDescriptor)

Convey("When generating a credential", func() {
vc, err := generator.Generate()
Convey("Then an error should be returned", func() {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, credential.NewVCError(credential.ErrGenerate, errors.New("failed")).Error())
So(vc, ShouldBeNil)
})
})
})
})

t.Run("with signer", func(t *testing.T) {
Convey("Given a credential generator with mocked descriptor", t, func() {
controller := gomock.NewController(t)
defer controller.Finish()

buf := bytes.NewBufferString(`{"@context":["https://www.w3.org/2018/credentials/v1"],"type":["VerifiableCredential"],"credentialSubject":{"id":"did:example:123"}}`)
mockDescriptor := testutil.NewMockDescriptor(controller)
mockDescriptor.EXPECT().Generate().Return(buf, nil).Times(1)
mockDescriptor.EXPECT().IssuedAt().Times(1)
mockDescriptor.EXPECT().ProofPurpose().Return("proof").Times(1)

mockSigner := testutil.NewMockSigner(controller)
mockSigner.EXPECT().Sign(gomock.Any()).Return([]byte("signature"), nil).Times(1)
mockSigner.EXPECT().Alg().AnyTimes()

loader, _ := testutil.MockDocumentLoader()

generator := credential.New(mockDescriptor,
credential.WithParser(credential.NewDefaultParser(loader)),
credential.WithSigner(mockSigner, "did:example:123"),
)

Convey("When generating a credential", func() {
vc, err := generator.Generate()
Convey("Then VC should be returned with proofs", func() {
So(err, ShouldBeNil)
So(vc, ShouldNotBeNil)
So(len(vc.Proofs), ShouldEqual, 1)
So(vc.Proofs[0]["verificationMethod"], ShouldEqual, "did:example:123")
So(vc.Proofs[0]["proofPurpose"], ShouldEqual, "proof")
})
})
})
})

t.Run("without signer", func(t *testing.T) {
Convey("Given a credential generator with mocked descriptor", t, func() {
controller := gomock.NewController(t)
defer controller.Finish()

buf := bytes.NewBufferString(`{"@context":["https://www.w3.org/2018/credentials/v1"],"type":["VerifiableCredential"],"credentialSubject":{"id":"did:example:123"}}`)
mockDescriptor := testutil.NewMockDescriptor(controller)
mockDescriptor.EXPECT().Generate().Return(buf, nil).Times(1)

loader, _ := testutil.MockDocumentLoader()

generator := credential.New(mockDescriptor,
credential.WithParser(credential.NewDefaultParser(loader)),
)

Convey("When generating a credential", func() {
vc, err := generator.Generate()
Convey("Then VC should be returned without proofs", func() {
So(err, ShouldBeNil)
So(vc, ShouldNotBeNil)
So(len(vc.Proofs), ShouldEqual, 0)
})
})
})
})
}
21 changes: 19 additions & 2 deletions credential/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,28 @@ type Parser[T Claim] interface {
ParseSigned(raw []byte) (T, error)
}

type credentialParser struct {
type DefaultParser struct {
documentLoader ld.DocumentLoader
}

func (cp *credentialParser) parseSigned(raw []byte) (*verifiable.Credential, error) {
func NewDefaultParser(documentLoader ld.DocumentLoader) *DefaultParser {
return &DefaultParser{documentLoader: documentLoader}
}

func (cp *DefaultParser) parse(raw []byte) (*verifiable.Credential, error) {
vc, err := verifiable.ParseCredential(
raw,
verifiable.WithJSONLDValidation(),
verifiable.WithPublicKeyFetcher(NewVDRKeyResolverWithSecp256k1(Secp256k1PubKeyFetcher).PublicKeyFetcher),
verifiable.WithJSONLDDocumentLoader(cp.documentLoader),
)
if err != nil {
return nil, NewVCError(ErrParse, err)
}
return vc, nil
}

func (cp *DefaultParser) parseSigned(raw []byte) (*verifiable.Credential, error) {
vc, err := verifiable.ParseCredential(
raw,
verifiable.WithJSONLDValidation(),
Expand Down
Loading

0 comments on commit 713c68e

Please sign in to comment.