From 508379a63009fadb940ff2adf2f0c6f862fc6660 Mon Sep 17 00:00:00 2001 From: Lyuben Penkovski <lyuben.penkovski@vereign.com> Date: Thu, 23 Jun 2022 19:52:03 +0300 Subject: [PATCH] Implement trusted import functionality --- cmd/infohub/main.go | 14 ++- go.mod | 4 +- internal/credential/issuer.go | 37 +++++- .../credential/keyfetcher/web_key_fetcher.go | 105 ++++++++++++++++++ .../infohub/infohubfakes/fake_cache.go | 87 +++++++++++++++ .../infohub/infohubfakes/fake_credentials.go | 84 ++++++++++++++ internal/service/infohub/service.go | 57 +++++++++- .../gaiax/tsa/golib/goadec/bytes_decoder.go | Bin 0 -> 758 bytes vendor/modules.txt | Bin 14889 -> 14929 bytes 9 files changed, 378 insertions(+), 10 deletions(-) create mode 100644 internal/credential/keyfetcher/web_key_fetcher.go create mode 100644 vendor/code.vereign.com/gaiax/tsa/golib/goadec/bytes_decoder.go diff --git a/cmd/infohub/main.go b/cmd/infohub/main.go index 441f173..6dd89fd 100644 --- a/cmd/infohub/main.go +++ b/cmd/infohub/main.go @@ -18,6 +18,7 @@ import ( "golang.org/x/sync/errgroup" "code.vereign.com/gaiax/tsa/golib/cache" + "code.vereign.com/gaiax/tsa/golib/goadec" "code.vereign.com/gaiax/tsa/golib/graceful" goahealth "code.vereign.com/gaiax/tsa/infohub/gen/health" goahealthsrv "code.vereign.com/gaiax/tsa/infohub/gen/http/health/server" @@ -41,7 +42,7 @@ func main() { // load configuration from environment var cfg config.Config if err := envconfig.Process("", &cfg); err != nil { - log.Fatalf("cannot load configuration: %v", err) + log.Fatalf("cannot load configuration: %v\n", err) } // create logger @@ -134,6 +135,17 @@ func main() { openapiServer = goaopenapisrv.New(openapiEndpoints, mux, dec, enc, nil, errFormatter, nil, nil) } + // set custom request decoder, so that request body bytes are simply + // read and not decoded in some other way + infohubServer.Import = goainfohubsrv.NewImportHandler( + infohubEndpoints.Import, + mux, + goadec.BytesDecoder, + enc, + nil, + errFormatter, + ) + // Configure the mux. goainfohubsrv.Mount(mux, infohubServer) goahealthsrv.Mount(mux, healthServer) diff --git a/go.mod b/go.mod index ad83f1d..e205b5e 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.17 require ( code.vereign.com/gaiax/tsa/golib v0.0.0-20220617105657-d5117fe7a1f4 + github.com/google/uuid v1.3.0 github.com/hyperledger/aries-framework-go v0.1.8 github.com/kelseyhightower/envconfig v1.4.0 github.com/piprate/json-gold v0.4.1 + github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 github.com/stretchr/testify v1.7.1 go.mongodb.org/mongo-driver v1.9.1 go.uber.org/zap v1.21.0 @@ -27,7 +29,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/tink/go v1.6.1-0.20210519071714-58be99b3c4d0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hyperledger/aries-framework-go/spi v0.0.0-20220322085443-50e8f9bd208b // indirect @@ -44,7 +45,6 @@ require ( github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/sergi/go-diff v1.2.0 // indirect github.com/smartystreets/assertions v1.13.0 // indirect - github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 // indirect github.com/teserakt-io/golang-ed25519 v0.0.0-20210104091850-3888c087a4c8 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.0.2 // indirect diff --git a/internal/credential/issuer.go b/internal/credential/issuer.go index b5e14fb..7f81064 100644 --- a/internal/credential/issuer.go +++ b/internal/credential/issuer.go @@ -4,27 +4,38 @@ import ( "net/http" "time" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020" "github.com/hyperledger/aries-framework-go/pkg/doc/util" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/piprate/json-gold/ld" + + "code.vereign.com/gaiax/tsa/infohub/internal/credential/keyfetcher" ) +var defaultContexts = []string{ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1", +} + type Issuer struct { - issuerURI string - docLoader *ld.CachingDocumentLoader + issuerURI string + docLoader *ld.CachingDocumentLoader + httpClient *http.Client } func NewIssuer(issuerURI string, httpClient *http.Client) *Issuer { loader := ld.NewDefaultDocumentLoader(httpClient) return &Issuer{ - issuerURI: issuerURI, - docLoader: ld.NewCachingDocumentLoader(loader), + issuerURI: issuerURI, + docLoader: ld.NewCachingDocumentLoader(loader), + httpClient: httpClient, } } func (i *Issuer) NewCredential(contexts []string, subjectID string, subject map[string]interface{}, proof bool) (*verifiable.Credential, error) { - jsonldContexts := []string{"https://www.w3.org/2018/credentials/v1"} + jsonldContexts := defaultContexts jsonldContexts = append(jsonldContexts, contexts...) vc := &verifiable.Credential{ @@ -42,7 +53,7 @@ func (i *Issuer) NewCredential(contexts []string, subjectID string, subject map[ } func (i *Issuer) NewPresentation(contexts []string, vc ...*verifiable.Credential) (*verifiable.Presentation, error) { - jsonldContexts := []string{"https://www.w3.org/2018/credentials/v1"} + jsonldContexts := defaultContexts jsonldContexts = append(jsonldContexts, contexts...) vp, err := verifiable.NewPresentation(verifiable.WithCredentials(vc...)) @@ -55,3 +66,17 @@ func (i *Issuer) NewPresentation(contexts []string, vc ...*verifiable.Credential return vp, nil } + +func (i *Issuer) ParsePresentation(vpBytes []byte) (*verifiable.Presentation, error) { + fetcher := keyfetcher.NewWebKeyFetcher(i.httpClient) + + return verifiable.ParsePresentation( + vpBytes, + verifiable.WithPresPublicKeyFetcher(fetcher), + verifiable.WithPresEmbeddedSignatureSuites( + jsonwebsignature2020.New(suite.WithVerifier(jsonwebsignature2020.NewPublicKeyVerifier())), + ), + verifiable.WithPresJSONLDDocumentLoader(i.docLoader), + verifiable.WithPresStrictValidation(), + ) +} diff --git a/internal/credential/keyfetcher/web_key_fetcher.go b/internal/credential/keyfetcher/web_key_fetcher.go new file mode 100644 index 0000000..736611b --- /dev/null +++ b/internal/credential/keyfetcher/web_key_fetcher.go @@ -0,0 +1,105 @@ +package keyfetcher + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + ariesjwk "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + "github.com/square/go-jose/v3" + + "code.vereign.com/gaiax/tsa/golib/errors" +) + +type VerificationMethod struct { + ID string `json:"id"` + Type string `json:"type"` + PublicKeyJWK *jose.JSONWebKey `json:"publicKeyJWK"` +} + +type WebKeyFetcher struct { + httpClient *http.Client +} + +// NewWebKeyFetcher retrieves a public key by directly calling an HTTP URL. +func NewWebKeyFetcher(httpClient *http.Client) verifiable.PublicKeyFetcher { + f := &WebKeyFetcher{httpClient: httpClient} + return f.fetch +} + +// fetch a public key directly from an HTTP URL. issuerID is expected to be +// URL like https://example.com/keys and keyID is the name of the key to be fetched. +// If the keyID contains a fragment(#), it is removed when constructing the target URL. +func (f *WebKeyFetcher) fetch(issuerID, keyID string) (*verifier.PublicKey, error) { + // If keyID is prefixed with hashtag(#) it must be removed + keyID = strings.TrimPrefix(keyID, "#") + + // Construct URL like http://signer:8080/v1/keys/key-1 + addr := fmt.Sprintf("%s/%s", issuerID, keyID) + uri, err := url.ParseRequestURI(addr) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", uri.String(), nil) + if err != nil { + return nil, err + } + + resp, err := f.httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return nil, errors.New(errors.NotFound, "key not found") + } + return nil, errors.New(errors.GetKind(resp.StatusCode), fmt.Errorf("unexpected response: %s", resp.Status)) + } + + var verificationMethod VerificationMethod + if err := json.NewDecoder(resp.Body).Decode(&verificationMethod); err != nil { + return nil, err + } + + if verificationMethod.PublicKeyJWK == nil { + return nil, fmt.Errorf("public key not found after decoding response") + } + + // We need to extract the Curve and Kty values as they are needed by the + // Aries public key verifiers. + curve, kty, err := keyParams(verificationMethod.PublicKeyJWK.Key) + if err != nil { + return nil, err + } + + return &verifier.PublicKey{ + Type: "JsonWebKey2020", + JWK: &ariesjwk.JWK{ + JSONWebKey: *verificationMethod.PublicKeyJWK, + Crv: curve, + Kty: kty, + }, + }, nil +} + +func keyParams(key interface{}) (curve string, kty string, err error) { + switch k := key.(type) { + case *ecdsa.PublicKey: + return k.Curve.Params().Name, "EC", nil + case *ed25519.PublicKey: + return "ED25519", "OKP", nil + case *rsa.PublicKey: + return "", "RSA", nil + default: + return "", "", fmt.Errorf("unknown key type: %T", k) + } +} diff --git a/internal/service/infohub/infohubfakes/fake_cache.go b/internal/service/infohub/infohubfakes/fake_cache.go index 2d15587..c96bb5f 100644 --- a/internal/service/infohub/infohubfakes/fake_cache.go +++ b/internal/service/infohub/infohubfakes/fake_cache.go @@ -25,6 +25,21 @@ type FakeCache struct { result1 []byte result2 error } + SetStub func(context.Context, string, string, string, []byte) error + setMutex sync.RWMutex + setArgsForCall []struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + arg5 []byte + } + setReturns struct { + result1 error + } + setReturnsOnCall map[int]struct { + result1 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -96,11 +111,83 @@ func (fake *FakeCache) GetReturnsOnCall(i int, result1 []byte, result2 error) { }{result1, result2} } +func (fake *FakeCache) Set(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 []byte) error { + var arg5Copy []byte + if arg5 != nil { + arg5Copy = make([]byte, len(arg5)) + copy(arg5Copy, arg5) + } + fake.setMutex.Lock() + ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)] + fake.setArgsForCall = append(fake.setArgsForCall, struct { + arg1 context.Context + arg2 string + arg3 string + arg4 string + arg5 []byte + }{arg1, arg2, arg3, arg4, arg5Copy}) + stub := fake.SetStub + fakeReturns := fake.setReturns + fake.recordInvocation("Set", []interface{}{arg1, arg2, arg3, arg4, arg5Copy}) + fake.setMutex.Unlock() + if stub != nil { + return stub(arg1, arg2, arg3, arg4, arg5) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeCache) SetCallCount() int { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + return len(fake.setArgsForCall) +} + +func (fake *FakeCache) SetCalls(stub func(context.Context, string, string, string, []byte) error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = stub +} + +func (fake *FakeCache) SetArgsForCall(i int) (context.Context, string, string, string, []byte) { + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() + argsForCall := fake.setArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 +} + +func (fake *FakeCache) SetReturns(result1 error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = nil + fake.setReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeCache) SetReturnsOnCall(i int, result1 error) { + fake.setMutex.Lock() + defer fake.setMutex.Unlock() + fake.SetStub = nil + if fake.setReturnsOnCall == nil { + fake.setReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.setReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeCache) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.getMutex.RLock() defer fake.getMutex.RUnlock() + fake.setMutex.RLock() + defer fake.setMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/service/infohub/infohubfakes/fake_credentials.go b/internal/service/infohub/infohubfakes/fake_credentials.go index 97a0a58..f08c93d 100644 --- a/internal/service/infohub/infohubfakes/fake_credentials.go +++ b/internal/service/infohub/infohubfakes/fake_credentials.go @@ -39,6 +39,19 @@ type FakeCredentials struct { result1 *verifiable.Presentation result2 error } + ParsePresentationStub func([]byte) (*verifiable.Presentation, error) + parsePresentationMutex sync.RWMutex + parsePresentationArgsForCall []struct { + arg1 []byte + } + parsePresentationReturns struct { + result1 *verifiable.Presentation + result2 error + } + parsePresentationReturnsOnCall map[int]struct { + result1 *verifiable.Presentation + result2 error + } invocations map[string][][]interface{} invocationsMutex sync.RWMutex } @@ -185,6 +198,75 @@ func (fake *FakeCredentials) NewPresentationReturnsOnCall(i int, result1 *verifi }{result1, result2} } +func (fake *FakeCredentials) ParsePresentation(arg1 []byte) (*verifiable.Presentation, error) { + var arg1Copy []byte + if arg1 != nil { + arg1Copy = make([]byte, len(arg1)) + copy(arg1Copy, arg1) + } + fake.parsePresentationMutex.Lock() + ret, specificReturn := fake.parsePresentationReturnsOnCall[len(fake.parsePresentationArgsForCall)] + fake.parsePresentationArgsForCall = append(fake.parsePresentationArgsForCall, struct { + arg1 []byte + }{arg1Copy}) + stub := fake.ParsePresentationStub + fakeReturns := fake.parsePresentationReturns + fake.recordInvocation("ParsePresentation", []interface{}{arg1Copy}) + fake.parsePresentationMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCredentials) ParsePresentationCallCount() int { + fake.parsePresentationMutex.RLock() + defer fake.parsePresentationMutex.RUnlock() + return len(fake.parsePresentationArgsForCall) +} + +func (fake *FakeCredentials) ParsePresentationCalls(stub func([]byte) (*verifiable.Presentation, error)) { + fake.parsePresentationMutex.Lock() + defer fake.parsePresentationMutex.Unlock() + fake.ParsePresentationStub = stub +} + +func (fake *FakeCredentials) ParsePresentationArgsForCall(i int) []byte { + fake.parsePresentationMutex.RLock() + defer fake.parsePresentationMutex.RUnlock() + argsForCall := fake.parsePresentationArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeCredentials) ParsePresentationReturns(result1 *verifiable.Presentation, result2 error) { + fake.parsePresentationMutex.Lock() + defer fake.parsePresentationMutex.Unlock() + fake.ParsePresentationStub = nil + fake.parsePresentationReturns = struct { + result1 *verifiable.Presentation + result2 error + }{result1, result2} +} + +func (fake *FakeCredentials) ParsePresentationReturnsOnCall(i int, result1 *verifiable.Presentation, result2 error) { + fake.parsePresentationMutex.Lock() + defer fake.parsePresentationMutex.Unlock() + fake.ParsePresentationStub = nil + if fake.parsePresentationReturnsOnCall == nil { + fake.parsePresentationReturnsOnCall = make(map[int]struct { + result1 *verifiable.Presentation + result2 error + }) + } + fake.parsePresentationReturnsOnCall[i] = struct { + result1 *verifiable.Presentation + result2 error + }{result1, result2} +} + func (fake *FakeCredentials) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() @@ -192,6 +274,8 @@ func (fake *FakeCredentials) Invocations() map[string][][]interface{} { defer fake.newCredentialMutex.RUnlock() fake.newPresentationMutex.RLock() defer fake.newPresentationMutex.RUnlock() + fake.parsePresentationMutex.RLock() + defer fake.parsePresentationMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/internal/service/infohub/service.go b/internal/service/infohub/service.go index c793ca6..d5b1299 100644 --- a/internal/service/infohub/service.go +++ b/internal/service/infohub/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" + "github.com/google/uuid" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "go.uber.org/zap" @@ -30,11 +31,13 @@ type Policy interface { type Cache interface { Get(ctx context.Context, key, namespace, scope string) ([]byte, error) + Set(ctx context.Context, key, namespace, scope string, value []byte) error } type Credentials interface { NewCredential(contexts []string, subjectID string, subject map[string]interface{}, proof bool) (*verifiable.Credential, error) NewPresentation(contexts []string, credentials ...*verifiable.Credential) (*verifiable.Presentation, error) + ParsePresentation(vpBytes []byte) (*verifiable.Presentation, error) } type Signer interface { @@ -61,8 +64,60 @@ func New(storage Storage, policy Policy, cache Cache, cred Credentials, signer S } } +// Import the given data wrapped as Verifiable Presentation into the Cache. +func (s *Service) Import(ctx context.Context, req *infohub.ImportRequest) (res *infohub.ImportResult, err error) { + logger := s.logger.With(zap.String("operation", "import")) + + vp, err := s.credentials.ParsePresentation(req.Data) + if err != nil { + logger.Error("error parsing verifiable presentation", zap.Error(err)) + return nil, err + } + + // separate data entries can be wrapped in separate verifiable credentials; + // each one of them must be placed separately in the cache + var importedCredentials []string + for _, credential := range vp.Credentials() { + cred, ok := credential.(map[string]interface{}) + if !ok { + logger.Warn("verifiable presentation contains unknown credential type") + return nil, errors.New(errors.BadRequest, "verifiable presentation contains unknown credential type") + } + + if cred["credentialSubject"] == nil { + logger.Error("verifiable credential doesn't contain subject") + return nil, errors.New(errors.BadRequest, "verifiable credential doesn't contain subject") + } + + subject, ok := cred["credentialSubject"].(map[string]interface{}) + if !ok { + logger.Error("verifiable credential subject is not a map object") + return nil, errors.New(errors.BadRequest, "verifiable credential subject is not a map object") + } + + subjectBytes, err := json.Marshal(subject) + if err != nil { + logger.Error("error encoding subject to json", zap.Error(err)) + return nil, errors.New("error encoding subject to json") + } + + importID := uuid.NewString() + if err := s.cache.Set(ctx, importID, "", "", subjectBytes); err != nil { + logger.Error("error saving imported data to cache", zap.Error(err)) + continue + } + importedCredentials = append(importedCredentials, importID) + } + + return &infohub.ImportResult{ImportIds: importedCredentials}, nil +} + func (s *Service) Export(ctx context.Context, req *infohub.ExportRequest) (interface{}, error) { - logger := s.logger.With(zap.String("exportName", req.ExportName)) + logger := s.logger.With( + zap.String("operation", "export"), + zap.String("exportName", req.ExportName), + ) + exportCfg, err := s.storage.ExportConfiguration(ctx, req.ExportName) if err != nil { logger.Error("error getting export configuration", zap.Error(err)) diff --git a/vendor/code.vereign.com/gaiax/tsa/golib/goadec/bytes_decoder.go b/vendor/code.vereign.com/gaiax/tsa/golib/goadec/bytes_decoder.go new file mode 100644 index 0000000000000000000000000000000000000000..b3ce113e4a8e168e391219377381dbf375f04b0e GIT binary patch literal 758 zcmZXSJ#XAF42EaxSFi?J$cMllx*513L5gM!g4>D?vP|D8aU@qC4i1O=?<M__#K>Zk z6v_7ysj7lUrC-z$;pkM08V2u1(_(<%PQerYOBf>DqjoL&P>NxbErfX;oR1(ymUpi) zr=M~2w=yn8)3akSR;L(;T!@Th*o2&nl61_`R%BM=&n2Z0qN?ET95F1i;4vm|11Jj* zhr=WJGgjl;GY~YhK|HC*mCuQQJ*qZP2)DTncP&MitllU{4?CtRmkSp>jVZ7qTtqfg zfW^1ZaMG7|gi(mA5b-=9Od4xhh(75ETn)xidz!K5)Cu8BtB;%lcAG{Y`aeF$FTMtU zeQzk+ZCSBwPL5z~MBgcmmums&z4J`f+^`Mxo|Ji86X9*&%eM4|$V@jKf7^pKJ?F1O zpdyi7MHb#n)F;r~VJsyNpx`{M0>X}NZ|t$AvYm!K%&u&x@qGU0FW=?;yb)Q8ol1S= z&@yg%Rpl?n?PRW%wJt6jUpHGrvLOxp46%h#^$CGFYWRN0Z$Oh8fv(FlJq_zW2!r|o literal 0 HcmV?d00001 diff --git a/vendor/modules.txt b/vendor/modules.txt index c042d0447de65efe01815982ff797e8bc84167fb..0e3f360843717aff94996dd0e5bc07478aa720b6 100644 GIT binary patch delta 19 bcmZ2ka<OE>8P@#7l+@&jjY=C&$5{dZUu_8o delta 10 Rcmcauva)2tnT=m!Ede5{1^NI0 -- GitLab