diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..39a926ef3cd2803c87177c6af047204b45373e4a --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +[](https://code.vereign.com/gaiax/tsa/infohub/-/commits/main) +[](https://code.vereign.com/gaiax/tsa/infohub/-/commits/main) + +# Information Hub + +Information Hub service is responsible for exporting and importing policy data +wrapped in Verifiable Credentials and Verifiable Presentations. + diff --git a/cmd/infohub/main.go b/cmd/infohub/main.go index 9a41fe3e3e7120b825848d499860651da93846f2..441f1734dc01399d7791708fc90b1fb8f6250e32 100644 --- a/cmd/infohub/main.go +++ b/cmd/infohub/main.go @@ -26,7 +26,7 @@ import ( goainfohub "code.vereign.com/gaiax/tsa/infohub/gen/infohub" "code.vereign.com/gaiax/tsa/infohub/gen/openapi" "code.vereign.com/gaiax/tsa/infohub/internal/clients/policy" - "code.vereign.com/gaiax/tsa/infohub/internal/clients/vault" + "code.vereign.com/gaiax/tsa/infohub/internal/clients/signer" "code.vereign.com/gaiax/tsa/infohub/internal/config" "code.vereign.com/gaiax/tsa/infohub/internal/credential" "code.vereign.com/gaiax/tsa/infohub/internal/service" @@ -72,27 +72,25 @@ func main() { logger.Fatal("error connecting to database", zap.Error(err)) } - vault, err := vault.New(cfg.Vault.Addr, cfg.Vault.Token, cfg.Vault.Keyname) - if err != nil { - logger.Fatal("error creating vault client", zap.Error(err)) - } - httpClient := httpClient() - credentials := credential.NewIssuer(cfg.Credential.IssuerName, cfg.Credential.Keyname, vault, httpClient) + credentials := credential.NewIssuer(cfg.Credential.IssuerURI, httpClient) // create policy client - policy := policy.New(cfg.Policy.Addr, httpClient) + policy := policy.New(cfg.Policy.Addr, policy.WithHTTPClient(httpClient)) // create cache client cache := cache.New(cfg.Cache.Addr) + // create signer client + signer := signer.New(cfg.Signer.Addr, signer.WithHTTPClient(httpClient)) + // create services var ( infohubSvc goainfohub.Service healthSvc goahealth.Service ) { - infohubSvc = infohub.New(storage, policy, cache, credentials, logger) + infohubSvc = infohub.New(storage, policy, cache, credentials, signer, logger) healthSvc = health.New() } diff --git a/internal/clients/policy/client.go b/internal/clients/policy/client.go index 77606b2cec5460bd979716a0929ef7aa5193a7d0..0d475d4af020f2eb7cf9899a78a47e710e89cad8 100644 --- a/internal/clients/policy/client.go +++ b/internal/clients/policy/client.go @@ -19,11 +19,17 @@ type Client struct { httpClient *http.Client } -func New(addr string, httpClient *http.Client) *Client { - return &Client{ +func New(addr string, opts ...ClientOption) *Client { + c := &Client{ addr: addr, - httpClient: httpClient, + httpClient: http.DefaultClient, } + + for _, opt := range opts { + opt(c) + } + + return c } // Evaluate calls the policy service to execute the given policy. diff --git a/internal/clients/policy/option.go b/internal/clients/policy/option.go new file mode 100644 index 0000000000000000000000000000000000000000..60ba73264daa40a6502fbd45229a2ddf07886c98 --- /dev/null +++ b/internal/clients/policy/option.go @@ -0,0 +1,13 @@ +package policy + +import ( + "net/http" +) + +type ClientOption func(*Client) + +func WithHTTPClient(client *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = client + } +} diff --git a/internal/clients/signer/client.go b/internal/clients/signer/client.go new file mode 100644 index 0000000000000000000000000000000000000000..84b18f190aa44b34f34201c4548f649bc7eb8ae3 --- /dev/null +++ b/internal/clients/signer/client.go @@ -0,0 +1,69 @@ +package signer + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + "github.com/piprate/json-gold/ld" +) + +const presentationProofPath = "/v1/presentation/proof" + +type Client struct { + addr string + httpClient *http.Client + docLoader *ld.CachingDocumentLoader +} + +func New(addr string, opts ...ClientOption) *Client { + c := &Client{ + addr: addr, + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(c) + } + + c.docLoader = ld.NewCachingDocumentLoader(ld.NewDefaultDocumentLoader(c.httpClient)) + + return c +} + +func (c *Client) PresentationProof(ctx context.Context, vp *verifiable.Presentation) (*verifiable.Presentation, error) { + vpBytes, err := json.Marshal(vp) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.addr+presentationProofPath, bytes.NewReader(vpBytes)) + if err != nil { + return nil, err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response from signer: %s", resp.Status) + } + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return verifiable.ParsePresentation( + respBytes, + verifiable.WithPresJSONLDDocumentLoader(c.docLoader), + verifiable.WithPresDisabledProofCheck(), + ) +} diff --git a/internal/clients/signer/option.go b/internal/clients/signer/option.go new file mode 100644 index 0000000000000000000000000000000000000000..4764ff4ef85b6235d8380757e9cee1f671f66ed1 --- /dev/null +++ b/internal/clients/signer/option.go @@ -0,0 +1,13 @@ +package signer + +import ( + "net/http" +) + +type ClientOption func(*Client) + +func WithHTTPClient(client *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = client + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 2153cfe50775a1d8b2491a0eb42bb38caa40621b..eeb98377afde41ffa7444c695d62e2ecc6c79de6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,8 +7,8 @@ type Config struct { Mongo mongoConfig Policy policyConfig Cache cacheConfig - Vault vaultConfig Credential credentialConfig + Signer signerConfig LogLevel string `envconfig:"LOG_LEVEL" default:"INFO"` } @@ -29,15 +29,8 @@ type mongoConfig struct { Collection string `envconfig:"MONGO_COLLECTION" default:"exports"` } -type vaultConfig struct { - Addr string `envconfig:"VAULT_ADDR" required:"true"` - Token string `envconfig:"VAULT_TOKEN" required:"true"` - Keyname string `envconfig:"VAULT_KEYNAME" required:"true"` -} - type credentialConfig struct { - IssuerName string `envconfig:"CRED_ISSUER_NAME" required:"true"` - Keyname string `envconfig:"CRED_KEYNAME" required:"true"` + IssuerURI string `envconfig:"ISSUER_URI" required:"true"` } type policyConfig struct { @@ -47,3 +40,7 @@ type policyConfig struct { type cacheConfig struct { Addr string `envconfig:"CACHE_ADDR" required:"true"` } + +type signerConfig struct { + Addr string `envconfig:"SIGNER_ADDR" required:"true"` +} diff --git a/internal/credential/issuer.go b/internal/credential/issuer.go index e621865ea7c393b7a6f7725496d843ad0bc2e461..b5e14fbcf06659149b31499a486498ad0a18c506 100644 --- a/internal/credential/issuer.go +++ b/internal/credential/issuer.go @@ -4,48 +4,22 @@ import ( "net/http" "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/ed25519signature2018" "github.com/hyperledger/aries-framework-go/pkg/doc/util" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/piprate/json-gold/ld" ) -type Signer interface { - Sign(data []byte) ([]byte, error) -} - type Issuer struct { - issuerName string - signer Signer - keyname string - - // proofContext is used to generate linked data proof - proofContext *verifiable.LinkedDataProofContext - docLoader *ld.CachingDocumentLoader + issuerURI string + docLoader *ld.CachingDocumentLoader } -func NewIssuer(issuerName string, keyname string, signer Signer, httpClient *http.Client) *Issuer { - sigSuite := ed25519signature2018.New( - suite.WithSigner(signer), - suite.WithVerifier(ed25519signature2018.NewPublicKeyVerifier())) - - proofContext := &verifiable.LinkedDataProofContext{ - Suite: sigSuite, - SignatureType: ed25519signature2018.SignatureType, - SignatureRepresentation: verifiable.SignatureProofValue, - VerificationMethod: keyname, - } - +func NewIssuer(issuerURI string, httpClient *http.Client) *Issuer { loader := ld.NewDefaultDocumentLoader(httpClient) return &Issuer{ - issuerName: issuerName, - signer: signer, - keyname: keyname, - docLoader: ld.NewCachingDocumentLoader(loader), - proofContext: proofContext, + issuerURI: issuerURI, + docLoader: ld.NewCachingDocumentLoader(loader), } } @@ -56,7 +30,7 @@ func (i *Issuer) NewCredential(contexts []string, subjectID string, subject map[ vc := &verifiable.Credential{ Context: jsonldContexts, Types: []string{verifiable.VCType}, - Issuer: verifiable.Issuer{ID: i.issuerName}, + Issuer: verifiable.Issuer{ID: i.issuerURI}, Issued: &util.TimeWrapper{Time: time.Now()}, Subject: verifiable.Subject{ ID: subjectID, @@ -64,12 +38,6 @@ func (i *Issuer) NewCredential(contexts []string, subjectID string, subject map[ }, } - if proof { - if err := vc.AddLinkedDataProof(i.proofContext, jsonld.WithDocumentLoader(i.docLoader)); err != nil { - return nil, err - } - } - return vc, nil } @@ -82,12 +50,8 @@ func (i *Issuer) NewPresentation(contexts []string, vc ...*verifiable.Credential return nil, err } vp.Context = jsonldContexts - vp.ID = i.issuerName + vp.ID = i.issuerURI vp.Type = []string{verifiable.VPType} - if err := vp.AddLinkedDataProof(i.proofContext, jsonld.WithDocumentLoader(i.docLoader)); err != nil { - return nil, err - } - return vp, nil } diff --git a/internal/service/infohub/service.go b/internal/service/infohub/service.go index b18b9e59a4299f51d1a9f932ab190d10984809ec..160426115888d7dbe8289d57bc5a1ec04c60a8c8 100644 --- a/internal/service/infohub/service.go +++ b/internal/service/infohub/service.go @@ -12,7 +12,7 @@ import ( "code.vereign.com/gaiax/tsa/infohub/internal/storage" ) -var exportAccepted = map[string]interface{}{"result": "accepted"} +var exportAccepted = map[string]interface{}{"result": "export request is accepted"} type Storage interface { ExportConfiguration(ctx context.Context, exportName string) (*storage.ExportConfiguration, error) @@ -31,20 +31,26 @@ type Credentials interface { NewPresentation(contexts []string, credentials ...*verifiable.Credential) (*verifiable.Presentation, error) } +type Signer interface { + PresentationProof(ctx context.Context, vp *verifiable.Presentation) (*verifiable.Presentation, error) +} + type Service struct { storage Storage policy Policy cache Cache credentials Credentials + signer Signer logger *zap.Logger } -func New(storage Storage, policy Policy, cache Cache, cred Credentials, logger *zap.Logger) *Service { +func New(storage Storage, policy Policy, cache Cache, cred Credentials, signer Signer, logger *zap.Logger) *Service { return &Service{ storage: storage, policy: policy, cache: cache, credentials: cred, + signer: signer, logger: logger, } } @@ -57,27 +63,29 @@ func (s *Service) Export(ctx context.Context, req *infohub.ExportRequest) (inter return nil, err } + // get policy names needed for the export + var policyNames []string + for name := range exportCfg.Policies { + policyNames = append(policyNames, name) + } + // get the results of all policies configured in the export - results := make(map[string][]byte) - for policy := range exportCfg.Policies { - res, err := s.cache.Get(ctx, exportCacheKey(req.ExportName, policy), "", "") - if err != nil { - if errors.Is(errors.NotFound, err) { - if err := s.triggerExport(ctx, exportCfg); err != nil { - logger.Error("error performing export", zap.Error(err)) - return nil, err - } - return exportAccepted, nil + policyResults, err := s.getExportData(ctx, req.ExportName, policyNames) + if err != nil { + if errors.Is(errors.NotFound, err) { + if err := s.triggerExport(ctx, exportCfg); err != nil { + logger.Error("error performing export", zap.Error(err)) + return nil, err } - logger.Error("failed to get policy result from cache", zap.Error(err)) - return nil, err + return exportAccepted, nil } - results[policy] = res + logger.Error("failed to get policy results from cache", zap.Error(err)) + return nil, err } - // create separate verifiable credential for each policy result + // wrap each policy result in a verifiable credential var creds []*verifiable.Credential - for policy, result := range results { + for policy, result := range policyResults { var res map[string]interface{} if err := json.Unmarshal(result, &res); err != nil { logger.Error("error decoding policy result as json", zap.Error(err)) @@ -93,16 +101,41 @@ func (s *Service) Export(ctx context.Context, req *infohub.ExportRequest) (inter creds = append(creds, cred) } - // bundle all credentials in a verifiable presentation with proof + // wrap all credentials in a verifiable presentation vp, err := s.credentials.NewPresentation(exportCfg.Contexts, creds...) if err != nil { logger.Error("failed to create verifiable presentation", zap.Error(err)) return nil, errors.New("error creating export", err) } + // get presentation proof from the signer + vp, err = s.signer.PresentationProof(ctx, vp) + if err != nil { + logger.Error("fail to get presentation proof", zap.Error(err)) + return nil, errors.New("error creating export", err) + } + return vp, nil } +// getExportData retrieves from Cache the serialized policy execution results. +// If result for a given policy name is not found in the Cache, a NotFound error +// is returned. +// If all results are found, they are returned as map, where the key is policyName +// and the value is the JSON serialized bytes of the policy result. +func (s *Service) getExportData(ctx context.Context, exportName string, policyNames []string) (map[string][]byte, error) { + results := make(map[string][]byte) + for _, policy := range policyNames { + res, err := s.cache.Get(ctx, exportCacheKey(exportName, policy), "", "") + if err != nil { + return nil, err + } + results[policy] = res + } + + return results, nil +} + func (s *Service) triggerExport(ctx context.Context, exportCfg *storage.ExportConfiguration) error { s.logger.Info("export triggered", zap.String("exportName", exportCfg.ExportName)) for policy, input := range exportCfg.Policies {