diff --git a/cmd/infohub/main.go b/cmd/infohub/main.go index 441f1734dc01399d7791708fc90b1fb8f6250e32..6dd89fd314493126d0750409a4679d8314f9def4 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 ad83f1d94f64b5bc064d1075f699d4587bb0cf22..e205b5e28f007f4775b006431bd4554e79881330 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 b5e14fbcf06659149b31499a486498ad0a18c506..7f81064022e63ba4d52cddb9a9205e661ee56744 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 0000000000000000000000000000000000000000..736611b5ab055dce7f6f45a5a0f84a24ad20f979 --- /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 2d1558707e875060e1c6f470af7dfc8ef727a0e2..c96bb5fc1fbb7412838d29dc1435d553a42b6ee0 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 97a0a58cb5d1d02cfb710d17f346b3f83ea0b179..f08c93d50c19d0baa84c8eb75cd69247566cff73 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 c793ca6eda63d086f0e4fea7fff2ca14a7fa6430..d5b1299653112b5148504e7fe96195aaf0227390 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 Binary files /dev/null and b/vendor/code.vereign.com/gaiax/tsa/golib/goadec/bytes_decoder.go differ diff --git a/vendor/modules.txt b/vendor/modules.txt index c042d0447de65efe01815982ff797e8bc84167fb..0e3f360843717aff94996dd0e5bc07478aa720b6 100644 Binary files a/vendor/modules.txt and b/vendor/modules.txt differ