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

Implement trusted import functionality

parent eaf8aa99
No related branches found
No related tags found
No related merge requests found
Pipeline #51796 failed with stages
in 1 minute
......@@ -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
......
......@@ -3,7 +3,7 @@ package infohub
import (
"context"
"encoding/json"
"github.com/google/uuid"
"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
"go.uber.org/zap"
......@@ -30,11 +30,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 +63,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.
Finish editing this message first!
Please register or to comment