Skip to content
Snippets Groups Projects
Commit 508379a6 authored by Lyuben Penkovski's avatar Lyuben Penkovski
Browse files

Implement trusted import functionality

parent eaf8aa99
Branches
Tags
1 merge request!6HTTP endpoint to import trusted data
Pipeline #51797 passed
......@@ -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)
......
......@@ -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
......
......@@ -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(),
)
}
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)
}
}
......@@ -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
......
......@@ -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
......
......@@ -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))
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment