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

Use the signer service for VC and VP proofs

parent 0eda03e7
No related branches found
No related tags found
No related merge requests found
Pipeline #51498 failed with stage
in 42 seconds
[![pipeline status](https://code.vereign.com/gaiax/tsa/infohub/badges/main/pipeline.svg)](https://code.vereign.com/gaiax/tsa/infohub/-/commits/main)
[![coverage report](https://code.vereign.com/gaiax/tsa/infohub/badges/main/coverage.svg)](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.
......@@ -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()
}
......
......@@ -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.
......
package policy
import (
"net/http"
)
type ClientOption func(*Client)
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = client
}
}
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(),
)
}
package signer
import (
"net/http"
)
type ClientOption func(*Client)
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.httpClient = client
}
}
......@@ -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"`
}
......@@ -4,48 +4,23 @@ 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
keyname 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 +31,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 +39,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 +51,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
}
......@@ -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 {
......
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