diff --git a/.gitignore b/.gitignore index 10bce5a3..ce90322e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ policy/cmd/polcli/polcli protogen provisioning/cmd/provisioning-service/provisioning-service scheme/bin +store-plugin/bin plugin/test/bin scheme/enacttrust/test/tokens/*.token scheme/tpm-enacttrust/test/cmd/gen-token/gen-token diff --git a/Makefile b/Makefile index 59e9362c..66b13130 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ SUBDIR += builtin SUBDIR += auth SUBDIR += config SUBDIR += coserv +SUBDIR += store-plugin SUBDIR += handler SUBDIR += kvstore SUBDIR += log @@ -47,9 +48,10 @@ IGNORE_COVERAGE += github.com/veraison/services/vts/trustedservices IGNORE_COVERAGE += github.com/veraison/services/vtsclient IGNORE_COVERAGE += github.com/veraison/services/coserv/api IGNORE_COVERAGE += github.com/veraison/services/coserv/cmd/coserv-service -IGNORE_COVERAGE += github.com/veraison/services/scheme/amd-kds-coserv -IGNORE_COVERAGE += github.com/veraison/services/scheme/nvidia-coserv +IGNORE_COVERAGE += github.com/veraison/services/store-plugin/amd-kds-coserv +IGNORE_COVERAGE += github.com/veraison/services/store-plugin/nvidia-coserv IGNORE_COVERAGE += github.com/veraison/services/scheme/nvidia +IGNORE_COVERAGE += github.com/veraison/services/store-plugin/corim-store IGNORE_COVERAGE += github.com/veraison/services/scheme/parsec-cca IGNORE_COVERAGE += github.com/veraison/services/scheme/parsec-tpm IGNORE_COVERAGE += github.com/veraison/services/scheme/psa-iot diff --git a/builtin/builtin_loader.go b/builtin/builtin_loader.go index c81e2336..746a782d 100644 --- a/builtin/builtin_loader.go +++ b/builtin/builtin_loader.go @@ -10,11 +10,12 @@ import ( "go.uber.org/zap" ) -var defaultBuiltinLoader *BuiltinLoader +var defaultBuiltinSchemeLoader *BuiltinLoader type BuiltinLoader struct { logger *zap.SugaredLogger + class PluginClass loadedByName map[string]plugin.IPluggable loadedByMediaType map[string]plugin.IPluggable @@ -23,16 +24,20 @@ type BuiltinLoader struct { registeredPluginTypes map[string]string } -func NewBuiltinLoader(logger *zap.SugaredLogger) *BuiltinLoader { - return &BuiltinLoader{logger: logger} +func NewBuiltinLoader(logger *zap.SugaredLogger, class PluginClass) *BuiltinLoader { + return &BuiltinLoader{ + logger: logger, + class: class, + } } func CreateBuiltinLoader( cfg map[string]any, + class PluginClass, pluginParams map[string]*plugin.Parameters, logger *zap.SugaredLogger, ) (*BuiltinLoader, error) { - loader := NewBuiltinLoader(logger) + loader := NewBuiltinLoader(logger, class) err := loader.Init(cfg, pluginParams) return loader, err } @@ -70,11 +75,11 @@ func (o *BuiltinLoader) GetRegisteredMediaTypesByCategory(category string) []str } func DiscoverBuiltin[I plugin.IPluggable]() error { - return DiscoverBuiltinUsing[I](defaultBuiltinLoader) + return DiscoverBuiltinUsing[I](defaultBuiltinSchemeLoader) } func DiscoverBuiltinUsing[I plugin.IPluggable](loader *BuiltinLoader) error { - for _, p := range plugins { + for _, p := range plugins[loader.class] { _, ok := p.(I) if !ok { continue @@ -117,7 +122,7 @@ func DiscoverBuiltinUsing[I plugin.IPluggable](loader *BuiltinLoader) error { } func GetBuiltinHandleByMediaType[I plugin.IPluggable](mediaType string) (I, error) { - return GetBuiltinHandleByMediaTypeUsing[I](defaultBuiltinLoader, mediaType) + return GetBuiltinHandleByMediaTypeUsing[I](defaultBuiltinSchemeLoader, mediaType) } func GetBuiltinHandleByMediaTypeUsing[I plugin.IPluggable]( @@ -196,5 +201,5 @@ func GetBuiltinHandleByAttestationSchemeUsing[I plugin.IPluggable]( } func init() { - defaultBuiltinLoader = NewBuiltinLoader(log.Named("builtin")) + defaultBuiltinSchemeLoader = NewBuiltinLoader(log.Named("builtin"), SchemePlugin) } diff --git a/builtin/builtin_manager.go b/builtin/builtin_manager.go index 5c42cf6f..1fa1ab95 100644 --- a/builtin/builtin_manager.go +++ b/builtin/builtin_manager.go @@ -23,6 +23,7 @@ func NewBuiltinManager[I plugin.IPluggable]( func CreateBuiltinManager[I plugin.IPluggable]( v *viper.Viper, + class PluginClass, pluginParams map[string]*plugin.Parameters, logger *zap.SugaredLogger, name string, @@ -32,7 +33,7 @@ func CreateBuiltinManager[I plugin.IPluggable]( return nil, err } - loader, err := CreateBuiltinLoader(subs["builtin"].AllSettings(), pluginParams, logger) + loader, err := CreateBuiltinLoader(subs["builtin"].AllSettings(), class, pluginParams, logger) if err != nil { return nil, err } diff --git a/builtin/schemes.go b/builtin/plugins.go similarity index 69% rename from builtin/schemes.go rename to builtin/plugins.go index 5f8e8674..e197c1a2 100644 --- a/builtin/schemes.go +++ b/builtin/plugins.go @@ -6,18 +6,31 @@ import ( "github.com/veraison/services/handler" "github.com/veraison/services/plugin" - scheme9 "github.com/veraison/services/scheme/amd-kds-coserv" scheme3 "github.com/veraison/services/scheme/arm-cca" scheme10 "github.com/veraison/services/scheme/da-spdm" - scheme8 "github.com/veraison/services/scheme/nvidia-coserv" scheme1 "github.com/veraison/services/scheme/parsec-cca" scheme5 "github.com/veraison/services/scheme/parsec-tpm" scheme6 "github.com/veraison/services/scheme/psa-iot" scheme7 "github.com/veraison/services/scheme/sevsnp" scheme4 "github.com/veraison/services/scheme/tpm-enacttrust" + store3 "github.com/veraison/services/store-plugin/amd-kds-coserv" + store1 "github.com/veraison/services/store-plugin/corim-store" + store2 "github.com/veraison/services/store-plugin/nvidia-coserv" ) -var plugins = []plugin.IPluggable{ +type PluginClass uint8 + +const ( + SchemePlugin PluginClass = iota + StorePlugin +) + +var plugins = map[PluginClass][]plugin.IPluggable{ + SchemePlugin: schemePlugins, + StorePlugin: storePlugins, +} + +var schemePlugins = []plugin.IPluggable{ handler.MustNewSchemeImplementationWrapper(scheme1.Descriptor, scheme1.NewImplementation()), handler.MustNewSchemeImplementationWrapper(scheme3.Descriptor, scheme3.NewImplementation()), handler.MustNewSchemeImplementationWrapper(scheme4.Descriptor, scheme4.NewImplementation()), @@ -25,6 +38,10 @@ var plugins = []plugin.IPluggable{ handler.MustNewSchemeImplementationWrapper(scheme6.Descriptor, scheme6.NewImplementation()), handler.MustNewSchemeImplementationWrapper(scheme7.Descriptor, scheme7.NewImplementation()), handler.MustNewSchemeImplementationWrapper(scheme10.Descriptor, scheme10.NewImplementation()), - &scheme8.CoservProxyHandler{}, - &scheme9.CoservProxyHandler{}, +} + +var storePlugins = []plugin.IPluggable{ + &store2.CoservProxyHandler{}, + &store3.CoservProxyHandler{}, + store1.NewStore(), } diff --git a/deployments/docker/src/builder-dispatcher b/deployments/docker/src/builder-dispatcher index 2d05b067..82af8b73 100755 --- a/deployments/docker/src/builder-dispatcher +++ b/deployments/docker/src/builder-dispatcher @@ -25,11 +25,12 @@ function deploy() { local gobin=$(go env GOPATH)/bin echo "creating deployment directory structure under $DEPLOY_DIR" - mkdir -p $DEPLOY_DIR/plugins + mkdir -p $DEPLOY_DIR/plugins/schemes mkdir -p $DEPLOY_DIR/stores mkdir -p $DEPLOY_DIR/logs mkdir -p $DEPLOY_DIR/utils mkdir -p $DEPLOY_DIR/certs + mkdir -p $DEPLOY_DIR/plugins/stores echo "deploying services" cp $BUILD_DIR/provisioning/cmd/provisioning-service/provisioning-service $DEPLOY_DIR/ @@ -37,7 +38,8 @@ function deploy() { cp $BUILD_DIR/vts/cmd/vts-service/vts-service $DEPLOY_DIR/ cp $BUILD_DIR/management/cmd/management-service/management-service $DEPLOY_DIR/ cp $BUILD_DIR/coserv/cmd/coserv-service/coserv-service $DEPLOY_DIR/ - cp $BUILD_DIR/scheme/bin/* $DEPLOY_DIR/plugins/ + cp $BUILD_DIR/scheme/bin/* $DEPLOY_DIR/plugins/schemes/ + cp $BUILD_DIR/store-plugin/bin/* $DEPLOY_DIR/plugins/stores/ cp $BUILD_DIR/deployments/docker/src/skey.jwk $DEPLOY_DIR/ cp $BUILD_DIR/deployments/docker/src/coserv-signer.jwk $DEPLOY_DIR/ cp $BUILD_DIR/deployments/docker/src/service-entrypoint $DEPLOY_DIR/ diff --git a/deployments/docker/src/config.yaml.template b/deployments/docker/src/config.yaml.template index ffa30f73..92be250c 100644 --- a/deployments/docker/src/config.yaml.template +++ b/deployments/docker/src/config.yaml.template @@ -26,10 +26,6 @@ coserv: protocol: https cert: /opt/veraison/coserv.crt cert-key: /opt/veraison/coserv.key - max-expiry: 5 mins - signer: - alg: ES256 - key: /opt/veraison/coserv-signer.jwk vts: server-addr: vts-service:${VTS_PORT} tls: true @@ -42,11 +38,10 @@ ear-signer: key: skey.jwk plugin: backend: go-plugin - go-plugin: - dir: ./plugins/ -store: - dbms: sqlite - dsn: stores/vts/store.sql + go-plugin-schemes: + dir: ./plugins/schemes/ + go-plugin-stores: + dir: ./plugins/stores/ po-store: backend: sql sql: @@ -55,6 +50,20 @@ po-store: datasource: stores/vts/po-store.sql po-agent: backend: opa +endorsement-store: + coserv: + max-expiry: 5 mins + signer: + alg: ES256 + key: /opt/veraison/coserv-signer.jwk + active-plugins: + - corim-store + - nvidia-coserv-proxy-handler + - amd-kds-coserv-proxy-handler + plugin-parameters: + corim-store: + dbms: sqlite + dsn: /opt/veraison/stores/vts/store.sql auth: backend: keycloak host: keycloak-service diff --git a/deployments/native/README.md b/deployments/native/README.md index e41b12d7..cce3cc81 100644 --- a/deployments/native/README.md +++ b/deployments/native/README.md @@ -368,6 +368,8 @@ structure ├── env ├── logs ├── plugins +│   ├── schemes +│   └── stores ├── signing ├── stores └── systemd (or launchd) @@ -401,9 +403,10 @@ This directory contains service logs. #### `plugins` -This directly contains attestation scheme plugins. +This directly contains attestation scheme plugins (in the `schemes` sub directory) +and endorsement store plugins (in the `stores` sub directory). -(note: if the deployment was crated with `-s` option, the plugins will in fact +(note: if the deployment was created with `-s` option, the plugins will in fact be symlinks to their source locations.) #### `signing` diff --git a/deployments/native/config/services.yaml.template b/deployments/native/config/services.yaml.template index f032fe4b..85a559a7 100644 --- a/deployments/native/config/services.yaml.template +++ b/deployments/native/config/services.yaml.template @@ -26,10 +26,6 @@ coserv: protocol: https cert: ${VERAISON_CERTS_DIR}/coserv.crt cert-key: ${VERAISON_CERTS_DIR}/coserv.key - max-expiry: 5 mins - signer: - alg: ES256 - key: ${VERAISON_SIGNING_DIR}/coserv-signer.jwk vts: server-addr: ${VTS_HOST}:${VTS_PORT} tls: true @@ -42,8 +38,10 @@ ear-signer: key: ${VERAISON_SIGNING_DIR}/skey.jwk plugin: backend: go-plugin - go-plugin: - dir: ${VERAISON_PLUGINS_DIR}/ + go-plugin-schemes: + dir: ${VERAISON_SCHEME_PLUGINS_DIR}/ + go-plugin-stores: + dir: ${VERAISON_STORE_PLUGINS_DIR}/ ############################################################################## # The store entries below configure the stores for endorsements, trust # anchors, and policies. They are configured to use sqlite3 by default. sqlite3 @@ -58,17 +56,30 @@ plugin: # on localhost on its default port, the user is "veraison" with password # "password", and the relevant database is "veraison". ############################################################################## -store: - trace-sql: false +endorsement-store: + coserv: + max-expiry: 5 mins + signer: + alg: ES256 + key: ${VERAISON_SIGNING_DIR}/coserv-signer.jwk + active-plugins: + - corim-store + - nvidia-coserv-proxy-handler + - amd-kds-coserv-proxy-handler - dbms: sqlite3 - dsn: ${VERAISON_STORES_DIR}/store.sql + # configurations for the corim-store plugin + plugin-parameters: + corim-store: + trace-sql: false - #dbms: pgx - #dsn: postgres://veraison:password@localhost:5432/veraison - - #dbms: mysql - #dsn: veraison:password@tcp(localhost:3306)/veraison + dbms: sqlite3 + dsn: ${VERAISON_STORES_DIR}/store.sql + + #dbms: pgx + #dsn: postgres://veraison:password@localhost:5432/veraison + + #dbms: mysql + #dsn: veraison:password@tcp(localhost:3306)/veraison po-store: backend: sql sql: diff --git a/deployments/native/deployment.cfg b/deployments/native/deployment.cfg index eaf2a916..eba36c2e 100644 --- a/deployments/native/deployment.cfg +++ b/deployments/native/deployment.cfg @@ -53,4 +53,7 @@ VERAISON_SYSTEMD_USER_DIR=$(echo ${VERAISON_ROOT}/${VERAISON_SYSTEMD_USER_SUBDIR VERAISON_TMUX_DIR=$(echo ${VERAISON_ROOT}/${VERAISON_TMUX_SUBDIR:-} | tr -s '/') VERAISON_LICENSE_DIR=$(echo ${VERAISON_ROOT}/${VERAISON_LICENSE_SUBDIR:-} | tr -s '/') +VERAISON_SCHEME_PLUGINS_DIR=$(echo ${VERAISON_PLUGINS_DIR}/schemes | tr -s '/') +VERAISON_STORE_PLUGINS_DIR=$(echo ${VERAISON_PLUGINS_DIR}/stores | tr -s '/') + # vim: set ft=bash: diff --git a/deployments/native/deployment.sh b/deployments/native/deployment.sh index 1c202bb6..0ade63aa 100755 --- a/deployments/native/deployment.sh +++ b/deployments/native/deployment.sh @@ -27,7 +27,8 @@ SRC_LAUNCHD_DIR=${_THIS_DIR}/launchd SRC_CERTS_DIR=${VERAISON_CERTS:-${EXAMPLE_DIR}/certs} DEPLOYMENT_BIN_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_BIN_DIR} | tr -s '/') -DEPLOYMENT_PLUGINS_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_PLUGINS_DIR} | tr -s '/') +DEPLOYMENT_SCHEME_PLUGINS_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_SCHEME_PLUGINS_DIR} | tr -s '/') +DEPLOYMENT_STORE_PLUGINS_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_STORE_PLUGINS_DIR} | tr -s '/') DEPLOYMENT_CERTS_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_CERTS_DIR} | tr -s '/') DEPLOYMENT_LOGS_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_LOGS_DIR} | tr -s '/') DEPLOYMENT_STORES_DIR=$(echo ${DEPLOYMENT_DEST}/${VERAISON_STORES_DIR} | tr -s '/') @@ -433,7 +434,8 @@ function help() { function _init_deployment_dir() { mkdir -p ${DEPLOYMENT_BIN_DIR} - mkdir -p ${DEPLOYMENT_PLUGINS_DIR} + mkdir -p ${DEPLOYMENT_SCHEME_PLUGINS_DIR} + mkdir -p ${DEPLOYMENT_STORE_PLUGINS_DIR} mkdir -p ${DEPLOYMENT_CERTS_DIR} mkdir -p ${DEPLOYMENT_LOGS_DIR} mkdir -p ${DEPLOYMENT_CONFIG_DIR} @@ -547,7 +549,13 @@ function _symlink_bins() { while IFS= read -r -d '' path; do path=$(realpath "$path") chmod +x "$path" - ln -s $_f "$path" "${DEPLOYMENT_PLUGINS_DIR}/$(basename $path)" + ln -s $_f "$path" "${DEPLOYMENT_SCHEME_PLUGINS_DIR}/$(basename $path)" + done + + find "${ROOT_DIR}/store-plugin/bin/" -name '*.plugin' -print0 | grep -z -v handler | + while IFS= read -r -d '' path; do + chmod +x "$path" + ln -s $_f "$path" "${DEPLOYMENT_STORE_PLUGINS_DIR}/$(basename $path)" done } @@ -561,8 +569,14 @@ function _deploy_bins() { find "${ROOT_DIR}/scheme/bin/" -name '*.plugin' -print0 | grep -z -v handler | while IFS= read -r -d '' path; do - $_INSTALL -m 0755 "$path" "${DEPLOYMENT_PLUGINS_DIR}/$(basename $path)" + $_INSTALL -m 0755 "$path" "${DEPLOYMENT_SCHEME_PLUGINS_DIR}/$(basename $path)" done + + find "${ROOT_DIR}/endorsementstore/bin/" -name '*.plugin' -print0 | grep -z -v handler | + while IFS= read -r -d '' path; do + $_INSTALL -m 0755 "$path" "${DEPLOYMENT_STORE_PLUGINS_DIR}/$(basename $path)" + done + } function _deploy_frontend { diff --git a/handler/README.md b/handler/README.md index 058d23e8..9d85042d 100644 --- a/handler/README.md +++ b/handler/README.md @@ -1,8 +1,39 @@ -This package defines [`IEvidenceHandler`](ievidencehandler.go), -[`IEndorsementHandler`](iendorsementhandler.go), -[`IStoreHandler`](istorehandler.go) and [`ICoservHandler`](icoservhandler.go) -[pluggable](../plugin/README.md) interfaces and associated RPC channels. -These are used to add new attestation scheme to Veraison services. -Additionally, the package defines a [couple of wrappers](plugin.go) around +This package defines [`ISchemeHandler`](ischemehandler.go) and +[`IEndorsementStore`](iendorsementstore.go) interfaces and associated RPC +channels. These are used to add new attestation scheme and store backends +to Veraison services. Additionally, the package defines a +[couple of wrappers](plugin.go) around `plugin.RegisterImplementation` for registering implementations of these four interfaces. + + +## Notes on Endorsement Store interface + +The interface has two classes of methods: + +### Get endorsements + +1. `GetKeyTriples` and `GetValueTriples`: query for comid key and value +triples using comid environment map as input. These methods also take a label +as input, which is used to identify the attestation scheme. These methods +must return `ErrUnsupported` if the store backend does not support the method. +This could be because of either the store backend not implementing the method +or not supporting the attestation scheme. If the store backend supports the +method and the scheme, but no matches could be found, these methods must return +`ErrNotFound`. + +2. `ExecuteCoservQuery`: obtain CoSERV results as output using the base64url +encoded CoSERV query as input. This method also takes a profile as input, which +is used to identify the CoSERV profile. This method must return `ErrUnsupported` +if the store backend does not support the method or if the CoSERV profile is +not supported by the store backend. If the store backend supports the method and +the CoSERV profile, but no match could be found for the requested query, this +method must return `ErrNotFound`. This method should never return a CoSERV +containing empty results (empty AKQ or RVQ). + + +### Submit endorsements + +The method `AddCorimBytes` takes the CoRIM bytes as input along with the +scheme. The implementation must return `ErrUnsupported` if it does not support +the operation or the attestation scheme. diff --git a/handler/coservproxy_rpc.go b/handler/coservproxy_rpc.go deleted file mode 100644 index 1b0ee469..00000000 --- a/handler/coservproxy_rpc.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2025-2026 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package handler - -import ( - "encoding/json" - "fmt" - "net/rpc" - - "github.com/veraison/services/log" - "github.com/veraison/services/plugin" -) - -var CoservProxyHandlerRPC = &plugin.RPCChannel[ICoservProxyHandler]{ - GetClient: getCoservClient, - GetServer: getCoservServer, -} - -func getCoservClient(c *rpc.Client) interface{} { - return &CoservProxyRPCClient{client: c} -} - -func getCoservServer(i ICoservProxyHandler) interface{} { - return &CoservProxyRPCServer{Impl: i} -} - -type CoservProxyRPCServer struct { - Impl ICoservProxyHandler -} - -func (o *CoservProxyRPCServer) Init(args []byte, resp *any) error { - var params *plugin.Parameters - var err error - - if args == nil { - params = plugin.NewParameters() - } else { - if params, err = plugin.ParametersFromJSON(args); err != nil { - return err - } - } - - return o.Impl.Init(params) -} - -func (s *CoservProxyRPCServer) GetName(args interface{}, resp *string) error { - *resp = s.Impl.GetName() - return nil -} - -func (s *CoservProxyRPCServer) GetAttestationScheme(args interface{}, resp *string) error { - *resp = s.Impl.GetAttestationScheme() - return nil -} - -func (s *CoservProxyRPCServer) GetSupportedMediaTypes(args interface{}, resp *[]byte) error { - var err error - mts := s.Impl.GetSupportedMediaTypes() - - *resp, err = json.Marshal(mts) - return err -} - -type GetEndorsementArgs struct { - TenantID string - Query string -} - -func (s *CoservProxyRPCServer) GetEndorsements(args GetEndorsementArgs, resp *[]byte) (err error) { - *resp, err = s.Impl.GetEndorsements(args.TenantID, args.Query) - return err -} - -type CoservProxyRPCClient struct { - client *rpc.Client -} - -func (o *CoservProxyRPCClient) Init(params *plugin.Parameters) error { - var ( - unused any - args []byte - err error - ) - - if params != nil { - if args, err = params.MarshalJSON(); err != nil { - return err - } - } - - return o.client.Call("Plugin.Init", args, &unused) -} - -func (c *CoservProxyRPCClient) GetName() string { - var ( - resp string - unused interface{} - ) - - err := c.client.Call("Plugin.GetName", &unused, &resp) - if err != nil { - log.Errorf("Plugin.GetName RPC call failed: %v", err) - return "" - } - - return resp -} - -func (c *CoservProxyRPCClient) GetAttestationScheme() string { - var ( - resp string - unused interface{} - ) - - err := c.client.Call("Plugin.GetAttestationScheme", &unused, &resp) - if err != nil { - log.Errorf("Plugin.GetAttestationScheme RPC call failed: %v", err) - return "" - } - - return resp -} - -func (c *CoservProxyRPCClient) GetSupportedMediaTypes() map[string][]string { - var ( - resp []byte - unused any - ) - - err := c.client.Call("Plugin.GetSupportedMediaTypes", &unused, &resp) - if err != nil { - log.Errorf("Plugin.GetSupportedMediaTypes RPC call failed: %v", err) - return nil - } - - var ret map[string][]string - if err := json.Unmarshal(resp, &ret); err != nil { - log.Error(err) - } - - return ret -} - -func (c *CoservProxyRPCClient) GetEndorsements( - tenantID string, - query string, -) (resp []byte, err error) { - args := GetEndorsementArgs{TenantID: tenantID, Query: query} - - err = c.client.Call("Plugin.GetEndorsements", args, &resp) - if err != nil { - return nil, fmt.Errorf("Plugin.GetEndorsements RPC call failed: %w", ParseError(err)) - } - - return resp, nil -} diff --git a/handler/endorsementstore_rpc.go b/handler/endorsementstore_rpc.go new file mode 100644 index 00000000..07c7be98 --- /dev/null +++ b/handler/endorsementstore_rpc.go @@ -0,0 +1,267 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package handler + +import ( + "encoding/json" + "net/rpc" + + "github.com/fxamacker/cbor/v2" + "github.com/veraison/corim/comid" + "github.com/veraison/corim/coserv" + "github.com/veraison/services/log" + "github.com/veraison/services/plugin" + "github.com/veraison/services/proto" + "go.uber.org/zap" +) + +var EndorsementStoreRPC = &plugin.RPCChannel[IEndorsementStorePlugin]{ + GetClient: getStoreClient, + GetServer: getStoreServer, +} + +func getStoreServer(i IEndorsementStorePlugin) any { + return &StoreRPCServer{Impl: i} +} + +func getStoreClient(c *rpc.Client) any { + return &StoreRPCClient{ + client: c, + logger: log.Named("endorsementstore-rpc"), + } +} + +type StoreRPCServer struct { + Impl IEndorsementStorePlugin +} + +func (s *StoreRPCServer) Init(args []byte, resp *any) error { + var ( + params *plugin.Parameters + err error + ) + if args == nil { + params = plugin.NewParameters() + } else { + if params, err = plugin.ParametersFromJSON(args); err != nil { + return err + } + } + + return s.Impl.Init(params) +} + +func (s *StoreRPCServer) Fini(args any, resp *any) error { + return s.Impl.Fini() +} + +func (s *StoreRPCServer) GetName(args any, resp *string) error { + *resp = s.Impl.GetName() + return nil +} + +func (s *StoreRPCServer) GetAttestationScheme(args any, resp *string) error { + *resp = s.Impl.GetAttestationScheme() + return nil +} + +func (s *StoreRPCServer) GetSupportedMediaTypes(args any, resp *[]byte) error { + var err error + mts := s.Impl.GetSupportedMediaTypes() + + *resp, err = json.Marshal(mts) + return err +} + +func (s *StoreRPCServer) GetValueTriples(params *proto.GetEndorsementsArgs, resp *[]byte) error { + var env comid.Environment + if err := (&env).FromCBOR(params.Environment); err != nil { + return err + } + refVals, err := s.Impl.GetValueTriples(&env, params.Label, params.MatchExactly) + if err != nil { + return err + } + *resp, err = cbor.Marshal(refVals) + return err +} + +func (s *StoreRPCServer) GetKeyTriples(params *proto.GetEndorsementsArgs, resp *[]byte) error { + var env comid.Environment + if err := (&env).FromCBOR(params.Environment); err != nil { + return err + } + keys, err := s.Impl.GetKeyTriples(&env, params.Label, params.MatchExactly) + if err != nil { + return err + } + *resp, err = cbor.Marshal(keys) + return err +} + +func (s *StoreRPCServer) ExecuteCoservQuery(params *proto.EndorsementQueryIn, resp *[]byte) error { + cos, err := s.Impl.ExecuteCoservQuery(params.MediaType, params.Query) + if err != nil { + return err + } + *resp, err = cos.ToCBOR() + if err != nil { + return err + } + return nil +} + +func (s *StoreRPCServer) AddCorimBytes(params *proto.SubmitEndorsementsArgs, resp *any) error { + return s.Impl.AddCorimBytes(params.Endorsement, params.Label, params.Activate) +} + +type StoreRPCClient struct { + client *rpc.Client + logger *zap.SugaredLogger +} + +func (c *StoreRPCClient) Init(params *plugin.Parameters) error { + var ( + unused any + args []byte + err error + ) + + if params != nil { + if args, err = params.MarshalJSON(); err != nil { + return err + } + } + + return c.client.Call("Plugin.Init", args, &unused) +} + +func (c *StoreRPCClient) Fini() error { + var unused any + return c.client.Call("Plugin.Fini", &unused, &unused) +} + +func (c *StoreRPCClient) GetName() string { + var ( + resp string + unused any + ) + + err := c.client.Call("Plugin.GetName", &unused, &resp) + if err != nil { + log.Errorf("Plugin.GetName RPC call failed: %v", err) + return "" + } + + return resp +} + +func (c *StoreRPCClient) GetAttestationScheme() string { + var ( + resp string + unused any + ) + + err := c.client.Call("Plugin.GetAttestationScheme", &unused, &resp) + if err != nil { + log.Errorf("Plugin.GetAttestationScheme RPC call failed: %v", err) + return "" + } + + return resp +} + +func (c *StoreRPCClient) GetSupportedMediaTypes() map[string][]string { + var ( + resp []byte + unused any + ) + + err := c.client.Call("Plugin.GetSupportedMediaTypes", &unused, &resp) + if err != nil { + log.Errorf("Plugin.GetSupportedMediaTypes RPC call failed: %v", err) + return nil + } + + var ret map[string][]string + if err := json.Unmarshal(resp, &ret); err != nil { + log.Error(err) + } + + return ret +} + +func (c *StoreRPCClient) GetValueTriples(env *comid.Environment, label string, exact bool) ([]*comid.ValueTriple, error) { + c.logger.Debugw("value triples lookup", "environment", env) + envCbor, err := cbor.Marshal(env) + if err != nil { + return nil, err + } + args := proto.GetEndorsementsArgs{ + Environment: envCbor, + Label: label, + MatchExactly: exact, + } + var rawResp []byte + if err := c.client.Call("Plugin.GetValueTriples", &args, &rawResp); err != nil { + return nil, ParseError(err) + } + var ret []*comid.ValueTriple + if err := cbor.Unmarshal(rawResp, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func (c *StoreRPCClient) GetKeyTriples(env *comid.Environment, label string, exact bool) ([]*comid.KeyTriple, error) { + c.logger.Debugw("key triples lookup", "environment", env) + envCbor, err := cbor.Marshal(env) + if err != nil { + return nil, err + } + args := proto.GetEndorsementsArgs{ + Environment: envCbor, + Label: label, + MatchExactly: exact, + } + var rawResp []byte + if err := c.client.Call("Plugin.GetKeyTriples", &args, &rawResp); err != nil { + return nil, ParseError(err) + } + var ret []*comid.KeyTriple + if err := cbor.Unmarshal(rawResp, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func (c *StoreRPCClient) ExecuteCoservQuery(mediaType, query string) (*coserv.Coserv, error) { + c.logger.Debugw("coserv request", "media-type", mediaType, "query", query) + args := proto.EndorsementQueryIn{ + MediaType: mediaType, + Query: query, + } + + var rawResp []byte + if err := c.client.Call("Plugin.ExecuteCoservQuery", &args, &rawResp); err != nil { + return nil, ParseError(err) + } + var ret coserv.Coserv + if err := ret.FromCBOR(rawResp); err != nil { + return nil, err + } + return &ret, nil +} + +func (c *StoreRPCClient) AddCorimBytes(data []byte, label string, activate bool) error { + args := proto.SubmitEndorsementsArgs{ + Endorsement: data, + Label: label, + Activate: activate, + } + var unused any + if err := c.client.Call("Plugin.AddCorimBytes", &args, &unused); err != nil { + return ParseError(err) + } + return nil +} diff --git a/handler/error.go b/handler/error.go index c520c517..80936b4b 100644 --- a/handler/error.go +++ b/handler/error.go @@ -193,6 +193,14 @@ func ParseError(err error) error { return BadEvidenceError{msg[14:]} } + if msg == ErrNotFound.Error() { + return ErrNotFound + } + + if msg == ErrUnsupported.Error() { + return ErrUnsupported + } + var bee BadEvidenceError var decErr error if decErr = json.Unmarshal([]byte(msg), &bee); decErr == nil { diff --git a/handler/error_test.go b/handler/error_test.go index b2a177c0..e1243850 100644 --- a/handler/error_test.go +++ b/handler/error_test.go @@ -98,3 +98,13 @@ func Test_BadEvidenceError_unmarshalable(t *testing.T) { assert.True(t, ok) assert.Contains(t, parsed.Detail, "0x") } + +func Test_StoreErrorParse(t *testing.T) { + enf := errors.New(ErrNotFound.Error()) + eusp := errors.New(ErrUnsupported.Error()) + eother := errors.New("Other error") + + assert.ErrorIs(t, ParseError(enf), ErrNotFound) + assert.ErrorIs(t, ParseError(eusp), ErrUnsupported) + assert.Equal(t, ParseError(eother).Error(), eother.Error()) +} diff --git a/handler/icoservproxyhandler.go b/handler/icoservproxyhandler.go deleted file mode 100644 index ee631bcc..00000000 --- a/handler/icoservproxyhandler.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package handler - -import ( - "github.com/veraison/services/plugin" -) - -// ICoservProxyHandler defines the interface for CoSERV translation plugins -type ICoservProxyHandler interface { - plugin.IPluggable - - // GetEndorsements adds the "result set" to the input "query" CoSERV. - // The input query CoSERV is base64url-encoded. - // In case of a failure an error is returned and the CoSERV is nil. - GetEndorsements(tenantID string, query string) ([]byte, error) -} diff --git a/handler/iendorsementstore.go b/handler/iendorsementstore.go new file mode 100644 index 00000000..94e0ed56 --- /dev/null +++ b/handler/iendorsementstore.go @@ -0,0 +1,42 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package handler + +import ( + "errors" + + "github.com/veraison/corim/comid" + "github.com/veraison/corim/coserv" + "github.com/veraison/services/plugin" +) + +var ( + // When the store does not support the operation. + // Example 1: store supportes scheme A and C, but + // endorsements for scheme B is requested. + // + // Example 2: store does not support CoSERV but the + // ExecuteCoservQuery is called. + ErrUnsupported = errors.New("store does not support the operation") + + // Not found in store. This means the store supports the operation + // and the attestation scheme, but no matching values were found + // in the store. + ErrNotFound = errors.New("not found in store") +) + +type IEndorsementStorePlugin interface { + plugin.IPluggable + + IEndorsementStore +} + +type IEndorsementStore interface { + GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) + + GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) + + ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) + + AddCorimBytes(data []byte, scheme string, activate bool) error +} diff --git a/handler/plugin.go b/handler/plugin.go index c7ffc2a3..605e7701 100644 --- a/handler/plugin.go +++ b/handler/plugin.go @@ -6,13 +6,6 @@ import ( "github.com/veraison/services/plugin" ) -func RegisterCoservProxyHandler(i ICoservProxyHandler) { - err := plugin.RegisterImplementation("coserv-proxy-handler", i, CoservProxyHandlerRPC) - if err != nil { - panic(err) - } -} - func RegisterSchemeHandler(i ISchemeHandler) { err := plugin.RegisterImplementation("scheme-handler", i, SchemeHandlerRPC) if err != nil { @@ -28,3 +21,10 @@ func RegisterSchemeImplementation(desc SchemeDescriptor, i ISchemeImplementation RegisterSchemeHandler(wrapper) } + +func RegisterEndorsementStore(i IEndorsementStorePlugin) { + err := plugin.RegisterImplementation("endorsement-store", i, EndorsementStoreRPC) + if err != nil { + panic(err) + } +} diff --git a/handler/scheme_rpc.go b/handler/scheme_rpc.go index 4750a3db..53be4e4b 100644 --- a/handler/scheme_rpc.go +++ b/handler/scheme_rpc.go @@ -40,8 +40,8 @@ type SchemeRPCClient struct { func (o *SchemeRPCClient) Init(params *plugin.Parameters) error { var ( unused any - args []byte - err error + args []byte + err error ) if params != nil { @@ -53,6 +53,11 @@ func (o *SchemeRPCClient) Init(params *plugin.Parameters) error { return o.client.Call("Plugin.Init", args, &unused) } +func (o *SchemeRPCClient) Fini() error { + var unused any + return o.client.Call("Plugin.Fini", &unused, &unused) +} + func (o *SchemeRPCClient) GetName() string { var ( unused any @@ -296,6 +301,10 @@ func (o *SchemeRPCServer) Init(args []byte, resp *any) error { return o.Impl.Init(params) } +func (o *SchemeRPCServer) Fini(args any, resp *any) error { + return o.Impl.Fini() +} + func (o *SchemeRPCServer) GetName(unused any, resp *string) error { *resp = o.Impl.GetName() return nil diff --git a/handler/schemeimplementationwrapper.go b/handler/schemeimplementationwrapper.go index b8944545..97e289e4 100644 --- a/handler/schemeimplementationwrapper.go +++ b/handler/schemeimplementationwrapper.go @@ -58,6 +58,15 @@ func (o *SchemeImplementationWrapper) Init(params *plugin.Parameters) error { return nil } +func (o *SchemeImplementationWrapper) Fini() error { + if pluginImpl, ok := o.Impl.(interface { + Fini() error + }); ok { + return pluginImpl.Fini() + } + return nil +} + func (o *SchemeImplementationWrapper) GetName() string { return PluginNameFromScheme(o.Desc.Name) } diff --git a/management/cmd/management-service/README.md b/management/cmd/management-service/README.md index c1d700b7..dfb56a8e 100644 --- a/management/cmd/management-service/README.md +++ b/management/cmd/management-service/README.md @@ -59,6 +59,6 @@ po-agent: backend: opa plugin: backend: go-plugin - go-plugin: - folder: ../../plugins/bin/ + go-plugin-schemes: + dir: ../../plugins/bin/ ``` diff --git a/management/cmd/management-service/config.yaml b/management/cmd/management-service/config.yaml index 237c4cd2..963d7bf3 100644 --- a/management/cmd/management-service/config.yaml +++ b/management/cmd/management-service/config.yaml @@ -1,6 +1,6 @@ plugin: backend: go-plugin - go-plugin: + go-plugin-schemes: dir: ../../../scheme/bin/ po-store: backend: sql diff --git a/management/policy.go b/management/policy.go index a46c2fbd..2fcf895b 100644 --- a/management/policy.go +++ b/management/policy.go @@ -48,14 +48,14 @@ func CreatePolicyManagerFromConfig(v *viper.Viper, name string) (*PolicyManager, if config.SchemeLoader == "plugins" { // nolint:gocritic pluginManager, err = plugin.CreateGoPluginManager( - subs["plugin"], pluginConfig, log.Named("plugin"), + subs["plugin"], "go-plugin-schemes", pluginConfig, log.Named("plugin"), "scheme-handler", handler.SchemeHandlerRPC) if err != nil { log.Fatalf("plugin manager initialization failed: %v", err) } } else if config.SchemeLoader == "builtin" { pluginManager, err = builtin.CreateBuiltinManager[handler.ISchemeHandler]( - subs["plugin"], pluginConfig, log.Named("builtin"), "scheme-handler") + subs["plugin"], builtin.SchemePlugin, pluginConfig, log.Named("builtin"), "scheme-handler") if err != nil { log.Fatalf("scheme manager initialization failed: %v", err) } diff --git a/plugin/README.md b/plugin/README.md index 96a469f2..41d2668b 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -143,8 +143,17 @@ func main() { log.Fatal(err) } + // Extract plugin configuration + pluginConfig, err := plugin.ParametersMapFromViper(subs["plugin-type"], nil) + if err != nil { + log.Fatal(err) + } + pluginManager, err := plugin.CreateGoPluginManager( - subs["plugin"], log.Named("plugin"), + subs["plugin"], + "go-plugin-myplugin", // subs["plugin"]["go-plugin-myplugin"] must not be empty + pluginConfig, + log.Named("plugin"), // plugins must register themselves with type "my-plugin" -- see // above. "my-plugins", myplugin.MyPluginRPC) diff --git a/plugin/goplugin_context.go b/plugin/goplugin_context.go index f8ff2cb9..af3340b9 100644 --- a/plugin/goplugin_context.go +++ b/plugin/goplugin_context.go @@ -64,6 +64,9 @@ func (o PluginContext[I]) GetHandle() interface{} { } func (o PluginContext[I]) Close() { + if err := o.Handle.Fini(); err != nil { + log.Errorf("failed to Fini plugin %s: %v", o.Name, err) + } if o.client != nil { o.client.Kill() } diff --git a/plugin/goplugin_loader.go b/plugin/goplugin_loader.go index c0d57799..63eee95e 100644 --- a/plugin/goplugin_loader.go +++ b/plugin/goplugin_loader.go @@ -174,7 +174,8 @@ func DiscoverGoPluginUsing[I IPluggable](o *GoPluginLoader) error { o.logger.Debugw("initializing plugin", "plugin", pluginName, "params", params.Map()) if err := pluginContext.Handle.Init(params); err != nil { - o.logger.Errorf("plugin q: %s", pluginName, err.Error()) + o.logger.Errorf("plugin %q: %s", pluginName, err.Error()) + pluginContext.client.Kill() continue } diff --git a/plugin/goplugin_manager.go b/plugin/goplugin_manager.go index fc92ecb3..107e93e0 100644 --- a/plugin/goplugin_manager.go +++ b/plugin/goplugin_manager.go @@ -26,20 +26,33 @@ func NewGoPluginManager[I IPluggable]( return &GoPluginManager[I]{loader: loader, logger: logger} } +// CreateGoPluginManager create a new GoPluginManager for the provided plugin +// RPCChannel ch. In addition to the RPCChannel, it takes the the following +// additional inputs to find and load the plugins: +// +// - v - plugin configuration (section plugin in the config) +// - pluginClass - the class of plugins for which to create the manager. +// The parameters for loading this class of plugins will be part of +// 'pluginClass' section of the plugin configuration +// - pluginParams - parameters to be passed to this class of plugins +// - logger - logger that will be used by the plugin manager +// - name - the plugin implementation name that was registered +// - ch - the plugin RPC channel func CreateGoPluginManager[I IPluggable]( v *viper.Viper, + pluginClass string, pluginParams map[string]*Parameters, logger *zap.SugaredLogger, name string, ch *RPCChannel[I], ) (*GoPluginManager[I], error) { - subs, err := config.GetSubs(v, "go-plugin") + subs, err := config.GetSubs(v, pluginClass) if err != nil { return nil, err } - loader, err := CreateGoPluginLoader(subs["go-plugin"].AllSettings(), pluginParams, logger) + loader, err := CreateGoPluginLoader(subs[pluginClass].AllSettings(), pluginParams, logger) if err != nil { return nil, err } diff --git a/plugin/ipluggable.go b/plugin/ipluggable.go index 9a313540..f82ccae3 100644 --- a/plugin/ipluggable.go +++ b/plugin/ipluggable.go @@ -26,4 +26,8 @@ type IPluggable interface { // free to define their own categories -- as far as IPluggable goes, // categories are just arbitrary groupings of media types. GetSupportedMediaTypes() map[string][]string + + // Releases the resources used by the plugin. This method should be + // called before closing connection to the plugin. + Fini() error } diff --git a/plugin/parameters.go b/plugin/parameters.go index 5ec057e8..d27af719 100644 --- a/plugin/parameters.go +++ b/plugin/parameters.go @@ -14,7 +14,7 @@ import ( ) var ( - ErrNotSet = errors.New("parameter not set") + ErrNotSet = errors.New("parameter not set") ErrInvalid = errors.New("invalid parameter value") ) @@ -28,18 +28,18 @@ var ( // // For example: // -// v := viper.New() -// v.Set("s1.p1", "foo") -// v.Set("s2.p1", 1) -// m1, err := ParametersMapFromViper(v, func(n string) string { return strings.ToUpper(n) }) +// v := viper.New() +// v.Set("s1.p1", "foo") +// v.Set("s2.p1", 1) +// m1, err := ParametersMapFromViper(v, func(n string) string { return strings.ToUpper(n) }) // -// m2 := map[string]*Parameters{ -// "S1": NewParameters().SetString("p1", "foo"), -// "S2": NewParameters().SetInt("p1", 1), -// } +// m2 := map[string]*Parameters{ +// "S1": NewParameters().SetString("p1", "foo"), +// "S2": NewParameters().SetInt("p1", 1), +// } // // m1 and m2 above are equivalent. -func ParametersMapFromViper(v *viper.Viper, keyTrans func(string)string) (map[string]*Parameters, error) { +func ParametersMapFromViper(v *viper.Viper, keyTrans func(string) string) (map[string]*Parameters, error) { ret := make(map[string]*Parameters) if v == nil { return ret, nil @@ -180,7 +180,7 @@ func (o *Parameters) Set(key string, value any) error { case float64: // note: due to limited precision, float(9223372036854775807) // is 9223372036854775808.0, so we're using >= rather than > - // here. + // here. // This means that, in practice, the highest float64 // convertable to int64 is 9223372036854774784.0 // ((2^(63-52))/2), as anything above that would round up to @@ -201,6 +201,15 @@ func (o *Parameters) Set(key string, value any) error { return nil } +// Merge populates the Parameters from another Parameters +// object. Duplicate keys are overwritten +func (o *Parameters) Merge(p *Parameters) error { + if p == nil { + return nil + } + return o.PopulateFromMap(p.values) +} + // PopulateFromMap populates the Parameters from the provided map. This is // equivalent to calling Set for each key and corresponding value in the map. func (o *Parameters) PopulateFromMap(m map[string]any) error { @@ -334,7 +343,6 @@ func (o *Parameters) MustGetInt64(key string) int64 { return val } - // DefaultGetInt64 returns the int64 value corresponding to the specified key. // If the key is not set, defaultValue is returned instead. If the value is not // a int64, returns 0 and the error is set to ErrInvalid. diff --git a/plugin/parameters_test.go b/plugin/parameters_test.go index 2034823c..ffb3c164 100644 --- a/plugin/parameters_test.go +++ b/plugin/parameters_test.go @@ -10,14 +10,13 @@ import ( "github.com/stretchr/testify/assert" ) - func TestParameters_round_trip(t *testing.T) { params := NewParameters(). - SetString("p1", "foo"). - SetInt("p2", 1). - SetInt64("p3", 2). - SetBytes("p4", []byte{0xde, 0xad, 0xbe, 0xef}). - SetBool("p5", true) + SetString("p1", "foo"). + SetInt("p2", 1). + SetInt64("p3", 2). + SetBytes("p4", []byte{0xde, 0xad, 0xbe, 0xef}). + SetBool("p5", true) data, err := params.MarshalJSON() assert.NoError(t, err) @@ -174,3 +173,19 @@ func TestPametersMapFromViper(t *testing.T) { _, err = ParametersMapFromViper(v, nil) assert.ErrorIs(t, err, ErrInvalid) } + +func TestParametersMerge(t *testing.T) { + this := NewParameters().SetInt64("one", 1) + var other0 *Parameters // nil + other1 := NewParameters().SetInt64("two", 2) + + err := this.Merge(other0) + assert.NoError(t, err) + + err = this.Merge(other1) + assert.NoError(t, err) + + v, err := this.GetInt64("two") + assert.NoError(t, err) + assert.Equal(t, v, int64(2)) +} diff --git a/plugin/test/ammo.go b/plugin/test/ammo.go index f4b37cd0..ac346a4d 100644 --- a/plugin/test/ammo.go +++ b/plugin/test/ammo.go @@ -16,6 +16,7 @@ type IAmmo interface { GetAttestationScheme() string GetSupportedMediaTypes() map[string][]string GetCapacity() int + Fini() error } type AmmoRPCClient struct { @@ -25,8 +26,8 @@ type AmmoRPCClient struct { func (o *AmmoRPCClient) Init(params *plugin.Parameters) error { var ( unused any - args []byte - err error + args []byte + err error ) if params != nil { @@ -103,6 +104,15 @@ func (o *AmmoRPCClient) GetCapacity() int { return resp } +func (o *AmmoRPCClient) Fini() error { + var unused any + err := o.client.Call("Plugin.Fini", &unused, &unused) + if err != nil { + log.Printf("Plugin.Fini RPC call failed: %v", err) + } + return err +} + type AmmoRPCServer struct { Impl IAmmo } @@ -145,6 +155,10 @@ func (o *AmmoRPCServer) GetCapacity(args any, resp *int) error { return nil } +func (o *AmmoRPCServer) Fini(args any, resp *any) error { + return o.Impl.Fini() +} + func GetAmmoClient(c *rpc.Client) any { return &AmmoRPCClient{client: c} } diff --git a/plugin/test/gascartridge/gascartridge.go b/plugin/test/gascartridge/gascartridge.go index 1aa02522..256b37a2 100644 --- a/plugin/test/gascartridge/gascartridge.go +++ b/plugin/test/gascartridge/gascartridge.go @@ -30,6 +30,10 @@ func (o GasCartridge) GetCapacity() int { return 500 } +func (o GasCartridge) Fini() error { + return nil +} + func main() { test.RegisterAmmoImplementation(&GasCartridge{}) plugin.Serve() diff --git a/plugin/test/loader_test.go b/plugin/test/loader_test.go index 0dafa310..28588297 100644 --- a/plugin/test/loader_test.go +++ b/plugin/test/loader_test.go @@ -23,7 +23,7 @@ func TestLoader_discover_and_load(t *testing.T) { pluginParams := map[string]*plugin.Parameters{ "Federation Starship Officer": plugin.NewParameters().SetString("sound", "zap"), - "Galactic Imperial Trooper": plugin.NewParameters().SetString("sound", "pew, pew"), + "Galactic Imperial Trooper": plugin.NewParameters().SetString("sound", "pew, pew"), } ldr, err := plugin.CreateGoPluginLoader(cfg, pluginParams, logger) diff --git a/plugin/test/mook.go b/plugin/test/mook.go index 0a23abe5..70cbb132 100644 --- a/plugin/test/mook.go +++ b/plugin/test/mook.go @@ -16,6 +16,7 @@ type IMook interface { GetAttestationScheme() string GetSupportedMediaTypes() map[string][]string Shoot() string + Fini() error } type MookRPCClient struct { @@ -25,8 +26,8 @@ type MookRPCClient struct { func (o *MookRPCClient) Init(params *plugin.Parameters) error { var ( unused any - args []byte - err error + args []byte + err error ) if params != nil { @@ -103,6 +104,15 @@ func (o *MookRPCClient) Shoot() string { return resp } +func (o *MookRPCClient) Fini() error { + var unused any + err := o.client.Call("Plugin.Fini", &unused, &unused) + if err != nil { + log.Printf("Plugin.Fini RPC call failed: %v", err) + } + return err +} + type MookRPCServer struct { Impl IMook } @@ -145,6 +155,10 @@ func (o *MookRPCServer) Shoot(args any, resp *string) error { return nil } +func (o *MookRPCServer) Fini(args any, resp *any) error { + return o.Impl.Fini() +} + func GetMookClient(c *rpc.Client) any { return &MookRPCClient{client: c} } diff --git a/plugin/test/powercell/powercell.go b/plugin/test/powercell/powercell.go index 6cd1bf0c..474a5412 100644 --- a/plugin/test/powercell/powercell.go +++ b/plugin/test/powercell/powercell.go @@ -30,6 +30,10 @@ func (o PowerCell) GetCapacity() int { return 12000000 } +func (o PowerCell) Fini() error { + return nil +} + func main() { test.RegisterAmmoImplementation(&PowerCell{}) plugin.Serve() diff --git a/plugin/test/redshirt/redshirt.go b/plugin/test/redshirt/redshirt.go index 025c95b7..4933ccdb 100644 --- a/plugin/test/redshirt/redshirt.go +++ b/plugin/test/redshirt/redshirt.go @@ -35,6 +35,10 @@ func (o RedShirt) Shoot() string { return fmt.Sprintf("phaser goes %q", o.sound) } +func (o RedShirt) Fini() error { + return nil +} + func main() { test.RegisterMookImplementation(&RedShirt{}) plugin.Serve() diff --git a/plugin/test/trooper/trooper.go b/plugin/test/trooper/trooper.go index 9b1b6642..4a17576f 100644 --- a/plugin/test/trooper/trooper.go +++ b/plugin/test/trooper/trooper.go @@ -36,6 +36,10 @@ func (o ImperialTrooper) Shoot() string { return fmt.Sprintf("blaster goes %q", o.sound) } +func (o ImperialTrooper) Fini() error { + return nil +} + func main() { test.RegisterMookImplementation(&ImperialTrooper{}) plugin.Serve() diff --git a/proto/Makefile b/proto/Makefile index caf76379..12a7073c 100644 --- a/proto/Makefile +++ b/proto/Makefile @@ -11,6 +11,7 @@ PROTOSRCS += vts.proto PROTOSRCS += status.proto PROTOSRCS += endorsement_query.proto PROTOSRCS += scheme.proto +PROTOSRCS += endorsement_store.proto lint-hook-pre: protogen protolint lint $(PROTOSRCS) diff --git a/proto/endorsement_store.pb.go b/proto/endorsement_store.pb.go new file mode 100644 index 00000000..7dd2df2c --- /dev/null +++ b/proto/endorsement_store.pb.go @@ -0,0 +1,251 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.21.12 +// source: endorsement_store.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetEndorsementsArgs struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Environment []byte `protobuf:"bytes,1,opt,name=environment,json=environment-map,proto3" json:"environment,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + MatchExactly bool `protobuf:"varint,3,opt,name=match_exactly,json=match-exactly,proto3" json:"match_exactly,omitempty"` +} + +func (x *GetEndorsementsArgs) Reset() { + *x = GetEndorsementsArgs{} + if protoimpl.UnsafeEnabled { + mi := &file_endorsement_store_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetEndorsementsArgs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetEndorsementsArgs) ProtoMessage() {} + +func (x *GetEndorsementsArgs) ProtoReflect() protoreflect.Message { + mi := &file_endorsement_store_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetEndorsementsArgs.ProtoReflect.Descriptor instead. +func (*GetEndorsementsArgs) Descriptor() ([]byte, []int) { + return file_endorsement_store_proto_rawDescGZIP(), []int{0} +} + +func (x *GetEndorsementsArgs) GetEnvironment() []byte { + if x != nil { + return x.Environment + } + return nil +} + +func (x *GetEndorsementsArgs) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *GetEndorsementsArgs) GetMatchExactly() bool { + if x != nil { + return x.MatchExactly + } + return false +} + +type SubmitEndorsementsArgs struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Endorsement []byte `protobuf:"bytes,1,opt,name=endorsement,json=endorsements,proto3" json:"endorsement,omitempty"` + Label string `protobuf:"bytes,2,opt,name=label,proto3" json:"label,omitempty"` + Activate bool `protobuf:"varint,3,opt,name=activate,proto3" json:"activate,omitempty"` +} + +func (x *SubmitEndorsementsArgs) Reset() { + *x = SubmitEndorsementsArgs{} + if protoimpl.UnsafeEnabled { + mi := &file_endorsement_store_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubmitEndorsementsArgs) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubmitEndorsementsArgs) ProtoMessage() {} + +func (x *SubmitEndorsementsArgs) ProtoReflect() protoreflect.Message { + mi := &file_endorsement_store_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubmitEndorsementsArgs.ProtoReflect.Descriptor instead. +func (*SubmitEndorsementsArgs) Descriptor() ([]byte, []int) { + return file_endorsement_store_proto_rawDescGZIP(), []int{1} +} + +func (x *SubmitEndorsementsArgs) GetEndorsement() []byte { + if x != nil { + return x.Endorsement + } + return nil +} + +func (x *SubmitEndorsementsArgs) GetLabel() string { + if x != nil { + return x.Label + } + return "" +} + +func (x *SubmitEndorsementsArgs) GetActivate() bool { + if x != nil { + return x.Activate + } + return false +} + +var File_endorsement_store_proto protoreflect.FileDescriptor + +var file_endorsement_store_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x77, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x41, 0x72, 0x67, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x65, 0x6e, + 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x2d, 0x6d, 0x61, 0x70, 0x12, 0x14, 0x0a, + 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x65, 0x78, 0x61, + 0x63, 0x74, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x2d, 0x65, 0x78, 0x61, 0x63, 0x74, 0x6c, 0x79, 0x22, 0x6d, 0x0a, 0x16, 0x53, 0x75, 0x62, + 0x6d, 0x69, 0x74, 0x45, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x41, + 0x72, 0x67, 0x73, 0x12, 0x21, 0x0a, 0x0b, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x73, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x1a, 0x0a, 0x08, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x65, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x65, 0x72, 0x61, 0x69, 0x73, 0x6f, 0x6e, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_endorsement_store_proto_rawDescOnce sync.Once + file_endorsement_store_proto_rawDescData = file_endorsement_store_proto_rawDesc +) + +func file_endorsement_store_proto_rawDescGZIP() []byte { + file_endorsement_store_proto_rawDescOnce.Do(func() { + file_endorsement_store_proto_rawDescData = protoimpl.X.CompressGZIP(file_endorsement_store_proto_rawDescData) + }) + return file_endorsement_store_proto_rawDescData +} + +var file_endorsement_store_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_endorsement_store_proto_goTypes = []interface{}{ + (*GetEndorsementsArgs)(nil), // 0: proto.GetEndorsementsArgs + (*SubmitEndorsementsArgs)(nil), // 1: proto.SubmitEndorsementsArgs +} +var file_endorsement_store_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_endorsement_store_proto_init() } +func file_endorsement_store_proto_init() { + if File_endorsement_store_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_endorsement_store_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetEndorsementsArgs); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_endorsement_store_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubmitEndorsementsArgs); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_endorsement_store_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_endorsement_store_proto_goTypes, + DependencyIndexes: file_endorsement_store_proto_depIdxs, + MessageInfos: file_endorsement_store_proto_msgTypes, + }.Build() + File_endorsement_store_proto = out.File + file_endorsement_store_proto_rawDesc = nil + file_endorsement_store_proto_goTypes = nil + file_endorsement_store_proto_depIdxs = nil +} diff --git a/proto/endorsement_store.pb.json.go b/proto/endorsement_store.pb.json.go new file mode 100644 index 00000000..786d5ad8 --- /dev/null +++ b/proto/endorsement_store.pb.json.go @@ -0,0 +1,40 @@ +// Code generated by protoc-gen-go-json. DO NOT EDIT. +// source: endorsement_store.proto + +package proto + +import ( + "google.golang.org/protobuf/encoding/protojson" +) + +// MarshalJSON implements json.Marshaler +func (msg *GetEndorsementsArgs) MarshalJSON() ([]byte, error) { + return protojson.MarshalOptions{ + UseEnumNumbers: false, + EmitUnpopulated: false, + UseProtoNames: false, + }.Marshal(msg) +} + +// UnmarshalJSON implements json.Unmarshaler +func (msg *GetEndorsementsArgs) UnmarshalJSON(b []byte) error { + return protojson.UnmarshalOptions{ + DiscardUnknown: false, + }.Unmarshal(b, msg) +} + +// MarshalJSON implements json.Marshaler +func (msg *SubmitEndorsementsArgs) MarshalJSON() ([]byte, error) { + return protojson.MarshalOptions{ + UseEnumNumbers: false, + EmitUnpopulated: false, + UseProtoNames: false, + }.Marshal(msg) +} + +// UnmarshalJSON implements json.Unmarshaler +func (msg *SubmitEndorsementsArgs) UnmarshalJSON(b []byte) error { + return protojson.UnmarshalOptions{ + DiscardUnknown: false, + }.Unmarshal(b, msg) +} diff --git a/proto/endorsement_store.proto b/proto/endorsement_store.proto new file mode 100644 index 00000000..12d92bb0 --- /dev/null +++ b/proto/endorsement_store.proto @@ -0,0 +1,20 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +syntax = "proto3"; + +package proto; + +option go_package = "github.com/veraison/services/proto"; + +message GetEndorsementsArgs { + bytes environment = 1 [json_name = "environment-map"]; + string label = 2 [json_name = "label"]; + bool match_exactly = 3 [json_name = "match-exactly"]; +} + +message SubmitEndorsementsArgs { + bytes endorsement = 1 [json_name = "endorsements"]; + string label = 2 [json_name = "label"]; + bool activate = 3 [json_name = "activate"]; +} diff --git a/scheme/Makefile b/scheme/Makefile index 07154058..651491b2 100644 --- a/scheme/Makefile +++ b/scheme/Makefile @@ -12,9 +12,6 @@ SUBDIR += psa-iot SUBDIR += sevsnp SUBDIR += tpm-enacttrust -SUBDIR += amd-kds-coserv -SUBDIR += nvidia-coserv - clean: ; $(RM) -rf ./bin include ../mk/subdir.mk diff --git a/store-plugin/Makefile b/store-plugin/Makefile new file mode 100644 index 00000000..11c57462 --- /dev/null +++ b/store-plugin/Makefile @@ -0,0 +1,11 @@ +# Copyright 2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + + +SUBDIR += amd-kds-coserv +SUBDIR += corim-store +SUBDIR += nvidia-coserv + +clean: ; $(RM) -rf ./bin + +include ../mk/subdir.mk diff --git a/store-plugin/amd-kds-coserv/Makefile b/store-plugin/amd-kds-coserv/Makefile new file mode 100644 index 00000000..20e7e7c1 --- /dev/null +++ b/store-plugin/amd-kds-coserv/Makefile @@ -0,0 +1,15 @@ +# Copyright 2025-2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +.DEFAULT_GOAL := test + +GOPKG := github.com/veraison/services/store-plugin/amd-kds-coserv +SRCS := $(wildcard *.go) + +SUBDIR += plugin + +include ../../mk/common.mk +include ../../mk/lint.mk +include ../../mk/pkg.mk +include ../../mk/subdir.mk +include ../../mk/test.mk diff --git a/scheme/amd-kds-coserv/README.md b/store-plugin/amd-kds-coserv/README.md similarity index 100% rename from scheme/amd-kds-coserv/README.md rename to store-plugin/amd-kds-coserv/README.md diff --git a/scheme/amd-kds-coserv/coserv_handler.go b/store-plugin/amd-kds-coserv/coserv_handler.go similarity index 73% rename from scheme/amd-kds-coserv/coserv_handler.go rename to store-plugin/amd-kds-coserv/coserv_handler.go index 4531d3d9..2a329e09 100644 --- a/scheme/amd-kds-coserv/coserv_handler.go +++ b/store-plugin/amd-kds-coserv/coserv_handler.go @@ -9,18 +9,20 @@ import ( "fmt" "io" "net/http" + "slices" "time" "github.com/veraison/corim/comid" "github.com/veraison/corim/coserv" + "github.com/veraison/services/handler" + "github.com/veraison/services/log" "github.com/veraison/services/plugin" + vtscoserv "github.com/veraison/services/vts/coserv" ) -type CoservProxyHandler struct{} - -var ( - dummyAuthority = []byte{0xab, 0xcd, 0xef} -) +type CoservProxyHandler struct { + *vtscoserv.StoreConfig +} func constructVcekUrl(instance *coserv.StatefulInstance) string { // TODO(paulhowardarm) - deduce the product name and TCB parameters from the @@ -50,7 +52,18 @@ func getVcekForInstance(instance *coserv.StatefulInstance) ([]byte, error) { return certBytes, nil } -func (s CoservProxyHandler) Init(*plugin.Parameters) error { +func (s *CoservProxyHandler) Init(params *plugin.Parameters) error { + var cfg vtscoserv.StoreConfig + if err := (&cfg).FromParams(params); err != nil { + log.Named(SchemeName).Warn("CoSERV configuration not passed") + s.StoreConfig = nil + } else { + s.StoreConfig = &cfg + } + return nil +} + +func (s CoservProxyHandler) Fini() error { return nil } @@ -73,14 +86,10 @@ func (s CoservProxyHandler) addTrustAnchorForInstance(i *coserv.StatefulInstance return err } - // TODO(paulhowardarm) - This authority is a dummy value. + // TODO(paulhowardarm) // We need some kind of cert here, representing this plug-in's authority to re-package from NVIDIA CoRIM // We probably also need an NVIDIA cert in the chain - authority, err := comid.NewCryptoKeyTaggedBytes(dummyAuthority) - - if err != nil { - return fmt.Errorf("failed to make authority tagged bytes: %v", err) - } + authority := s.Authority block := &pem.Block{ Type: "CERTIFICATE", @@ -109,7 +118,15 @@ func (s CoservProxyHandler) addTrustAnchorForInstance(i *coserv.StatefulInstance return nil } -func (s CoservProxyHandler) GetEndorsements(tenantID string, query string) ([]byte, error) { +func (s CoservProxyHandler) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + if s.StoreConfig == nil { + panic("received CoSERV request when CoSERV API is disabled") + } + + if !slices.Contains(SupportedCoservProfiles, profile) { + return nil, handler.ErrUnsupported + } + var q coserv.Coserv if err := q.FromBase64Url(query); err != nil { return nil, err @@ -146,14 +163,24 @@ func (s CoservProxyHandler) GetEndorsements(tenantID string, query string) ([]by } } - // Set expiry on the results - fairly arbitrary expiry time of 1 hour - coservResult.SetExpiry(time.Now().Add(time.Hour)) + coservResult.SetExpiry(time.Now().Add(s.MaxExpiry)) // Add all results into the top-level CoSERV object err := q.AddResults(coservResult) if err != nil { return nil, err } + return &q, nil +} + +func (o CoservProxyHandler) GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) { + return nil, handler.ErrUnsupported +} + +func (o CoservProxyHandler) GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) { + return nil, handler.ErrUnsupported +} - return q.ToCBOR() +func (b CoservProxyHandler) AddCorimBytes(data []byte, scheme string, activate bool) error { + return handler.ErrUnsupported } diff --git a/scheme/amd-kds-coserv/plugin/Makefile b/store-plugin/amd-kds-coserv/plugin/Makefile similarity index 62% rename from scheme/amd-kds-coserv/plugin/Makefile rename to store-plugin/amd-kds-coserv/plugin/Makefile index ea0015f6..defcd1d9 100644 --- a/scheme/amd-kds-coserv/plugin/Makefile +++ b/store-plugin/amd-kds-coserv/plugin/Makefile @@ -1,8 +1,8 @@ -# Copyright 2025 Contributors to the Veraison project. +# Copyright 2025-2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 PLUGIN := ../../bin/coserv-amd-kds.plugin -GOPKG := github.com/veraison/services/scheme/amd-kds-coserv +GOPKG := github.com/veraison/services/store-plugin/amd-kds-coserv SRCS := main.go include ../../../mk/common.mk diff --git a/scheme/nvidia-coserv/plugin/main.go b/store-plugin/amd-kds-coserv/plugin/main.go similarity index 64% rename from scheme/nvidia-coserv/plugin/main.go rename to store-plugin/amd-kds-coserv/plugin/main.go index b5ee6c78..37ad6db5 100644 --- a/scheme/nvidia-coserv/plugin/main.go +++ b/store-plugin/amd-kds-coserv/plugin/main.go @@ -4,12 +4,12 @@ package main import ( + scheme "github.com/veraison/services/store-plugin/amd-kds-coserv" "github.com/veraison/services/handler" "github.com/veraison/services/plugin" - scheme "github.com/veraison/services/scheme/nvidia-coserv" ) func main() { - handler.RegisterCoservProxyHandler(&scheme.CoservProxyHandler{}) + handler.RegisterEndorsementStore(&scheme.CoservProxyHandler{}) plugin.Serve() } diff --git a/scheme/amd-kds-coserv/scheme.go b/store-plugin/amd-kds-coserv/scheme.go similarity index 60% rename from scheme/amd-kds-coserv/scheme.go rename to store-plugin/amd-kds-coserv/scheme.go index 26967aaf..040f00ae 100644 --- a/scheme/amd-kds-coserv/scheme.go +++ b/store-plugin/amd-kds-coserv/scheme.go @@ -1,4 +1,4 @@ -// Copyright 2025 Contributors to the Veraison project. +// Copyright 2025-2026 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package amdkdscoserv @@ -9,4 +9,5 @@ var ( CoservMediaTypes = []string{ `application/coserv+cbor; profile="tag:github.com/veraison,2023:amd_kds_coserv_proxy#1.0.0"`, } + SupportedCoservProfiles = []string{"tag:github.com/veraison,2023:amd_kds_coserv_proxy#1.0.0"} ) diff --git a/scheme/nvidia-coserv/Makefile b/store-plugin/corim-store/Makefile similarity index 66% rename from scheme/nvidia-coserv/Makefile rename to store-plugin/corim-store/Makefile index 49f41e73..9bd6c83f 100644 --- a/scheme/nvidia-coserv/Makefile +++ b/store-plugin/corim-store/Makefile @@ -1,9 +1,9 @@ -# Copyright 2025 Contributors to the Veraison project. +# Copyright 2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 .DEFAULT_GOAL := test -GOPKG := github.com/veraison/services/scheme/nvidia-coserv +GOPKG := github.com/veraison/services/store-plugin/corim-store SRCS := $(wildcard *.go) SUBDIR += plugin diff --git a/store-plugin/corim-store/corimstore_enstore.go b/store-plugin/corim-store/corimstore_enstore.go new file mode 100644 index 00000000..5fd483fb --- /dev/null +++ b/store-plugin/corim-store/corimstore_enstore.go @@ -0,0 +1,148 @@ +// Copyright 2025-2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package corim_store + +import ( + "context" + "errors" + "strings" + + corimstore "github.com/veraison/corim-store/pkg/store" + "github.com/veraison/corim/comid" + "github.com/veraison/corim/coserv" + handler "github.com/veraison/services/handler" + "github.com/veraison/services/log" + "github.com/veraison/services/plugin" + vtscoserv "github.com/veraison/services/vts/coserv" + "go.uber.org/zap" +) + +const ( + PluginName = "corim-store" +) + +// implement the IEndorsementStore interface for corimstore.Store +type Store struct { + Store *corimstore.Store + logger *zap.SugaredLogger + CoservCfg *vtscoserv.StoreConfig +} + +func NewStore() *Store { + logger := log.Named(PluginName) + return &Store{nil, logger, nil} +} + +func (s *Store) GetKeyTriples(env *comid.Environment, label string, exact bool) ([]*comid.KeyTriple, error) { + res, err := s.Store.GetActiveKeyTriples(env, label, exact) + if errors.Is(err, corimstore.ErrNoMatch) { + return nil, handler.ErrNotFound + } + return res, err +} + +func (s *Store) GetValueTriples(env *comid.Environment, label string, exact bool) ([]*comid.ValueTriple, error) { + res, err := s.Store.GetActiveValueTriples(env, label, exact) + if errors.Is(err, corimstore.ErrNoMatch) { + return nil, handler.ErrNotFound + } + return res, err +} + +func (s *Store) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + // If reading CoSERV config failed during initialization, + // CoSERV interface would be disabled. + if s.CoservCfg == nil { + panic("received CoSERV request when CoSERV API is disabled") + } + s.logger.Infof("got coserv query: %v", query) + coservService := corimstore.NewCoSERVService(s.Store, s.CoservCfg.Authority, s.CoservCfg.MaxExpiry) + var q coserv.Coserv + if err := q.FromBase64Url(query); err != nil { + s.logger.Errorf("could not decode string to coserv: %v", err) + return nil, err + } + if err := coservService.UpdateCoSERV(&q); err != nil { + s.logger.Errorf("could not update coserv: %v", err) + return nil, err + } + if q.Results == nil { + return nil, errors.New("internal error: bad CoSERV result: result-set is nil") + } + // return ErrNotFound instead of empty results + if q.Results.AKQ == nil && q.Results.RVQ == nil { + return nil, handler.ErrNotFound + } + s.logger.Debugf("got coserv response: %v", q) + return &q, nil +} + +func (s *Store) AddCorimBytes(data []byte, label string, activate bool) error { + s.logger.Debugf("adding CoRIM") + return s.Store.AddBytes(data, label, activate) +} + +func (s *Store) Fini() error { + s.logger.Info("closing corimstore") + if s.Store == nil { + panic("attempted to close an uninitialized store") + } + if err := s.Store.Close(); err != nil { + s.logger.Errorf("Failed to close corim-store: %v", err) + return err + } + return nil +} + +func (s *Store) Init(params *plugin.Parameters) error { + s.logger.Debug("initializing default store") + if params == nil { + return errors.New("parameters are required for corimstore") + } + cfg, err := ConfigFromParameters(params, s.logger) + if err != nil { + s.logger.Errorf("Failed to load corim-store parameters: %v", err) + return err + } + s.CoservCfg = cfg.CoservConfig() + + s.logger.Debugf("connecting to %s store %s", cfg.DBMS, cfg.DSN) + + store, err := corimstore.Open(context.Background(), cfg.StoreConfig()) + if err != nil { + return err + } + + // The store must be initialized before it may be used. In general, we + // rely on the store being pointed to by DSN to be initialized prior + // to starting the VTS. For in-memory store this can never be the case, so we + // initialize it here. + if strings.Contains(cfg.DSN, ":memory:") { + if err := store.Init(); err != nil { + return err + } + } + + s.Store = store + return nil +} + +func (s *Store) GetName() string { + return PluginName +} + +func (s *Store) GetAttestationScheme() string { + // FIXME(dhanus): This store will in principle work for any schemes that + // support CoRIM endorsements. So returning a single scheme name does + // not make sense. The plugin interface must be updated to support plugins + // that are associated with multiple schemes. + return "" +} + +func (s *Store) GetSupportedMediaTypes() map[string][]string { + // FIXME(dhanus): This method does not make much sense for store plugin, + // but something similar is required to let VTS know the CoSERV profiles + // that are supported. The plugin interface must be updated to somehow + // incorporate this. + return nil +} diff --git a/store-plugin/corim-store/plugin/Makefile b/store-plugin/corim-store/plugin/Makefile new file mode 100644 index 00000000..50e959fe --- /dev/null +++ b/store-plugin/corim-store/plugin/Makefile @@ -0,0 +1,11 @@ +# Copyright 2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +PLUGIN := ../../bin/corimstore.plugin +GOPKG := github.com/veraison/services/store-plugin/corim-store +SRCS := main.go + +include ../../../mk/common.mk +include ../../../mk/plugin.mk +include ../../../mk/lint.mk +include ../../../mk/test.mk diff --git a/store-plugin/corim-store/plugin/main.go b/store-plugin/corim-store/plugin/main.go new file mode 100644 index 00000000..c309009a --- /dev/null +++ b/store-plugin/corim-store/plugin/main.go @@ -0,0 +1,14 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "github.com/veraison/services/store-plugin/corim-store" + "github.com/veraison/services/handler" + "github.com/veraison/services/plugin" +) + +func main() { + handler.RegisterEndorsementStore(corim_store.NewStore()) + plugin.Serve() +} diff --git a/vts/store/store.go b/store-plugin/corim-store/store.go similarity index 57% rename from vts/store/store.go rename to store-plugin/corim-store/store.go index 7006cf60..59e22284 100644 --- a/vts/store/store.go +++ b/store-plugin/corim-store/store.go @@ -1,6 +1,6 @@ // Copyright 2025-2026 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 -package store +package corim_store import ( "context" @@ -9,13 +9,39 @@ import ( "github.com/spf13/viper" corimstore "github.com/veraison/corim-store/pkg/store" "github.com/veraison/services/config" + "github.com/veraison/services/plugin" + vtscoserv "github.com/veraison/services/vts/coserv" "go.uber.org/zap" ) type Config struct { - DBMS string `mapstructure:"dbms"` - DSN string `mapstructure:"dsn"` - TraceSQL bool `mapstructure:"trace-sql" config:"zerodefault"` + DBMS string `mapstructure:"dbms"` + DSN string `mapstructure:"dsn"` + TraceSQL bool `mapstructure:"trace-sql" config:"zerodefault"` + CoservCfg *vtscoserv.StoreConfig `config:"zerodefault"` +} + +func ConfigFromParameters(params *plugin.Parameters, logger *zap.SugaredLogger) (*Config, error) { + var ( + cfg Config + coservCfg vtscoserv.StoreConfig + ) + + logger.Debug("creating corim-store config from plugin parameters") + loader := config.NewNonExclusiveLoader(&cfg) + if err := loader.LoadFromMap(params.Map()); err != nil { + return nil, err + } + + // CoSERV configuration not being present is not an error + if err := (&coservCfg).FromParams(params); err != nil { + logger.Warnf("error converting parameters to coserv config: %v", err) + cfg.CoservCfg = nil + } else { + cfg.CoservCfg = &coservCfg + } + + return &cfg, nil } func (o *Config) StoreConfig() *corimstore.Config { @@ -35,6 +61,10 @@ func (o *Config) StoreConfig() *corimstore.Config { return ret } +func (o *Config) CoservConfig() *vtscoserv.StoreConfig { + return o.CoservCfg +} + func New(v *viper.Viper, logger *zap.SugaredLogger) (*corimstore.Store, error) { var cfg Config diff --git a/scheme/amd-kds-coserv/Makefile b/store-plugin/nvidia-coserv/Makefile similarity index 65% rename from scheme/amd-kds-coserv/Makefile rename to store-plugin/nvidia-coserv/Makefile index 77b1b57f..e90d32b0 100644 --- a/scheme/amd-kds-coserv/Makefile +++ b/store-plugin/nvidia-coserv/Makefile @@ -1,9 +1,9 @@ -# Copyright 2025 Contributors to the Veraison project. +# Copyright 2025-2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 .DEFAULT_GOAL := test -GOPKG := github.com/veraison/services/scheme/amd-kds-coserv +GOPKG := github.com/veraison/services/store-plugin/nvidia-coserv SRCS := $(wildcard *.go) SUBDIR += plugin diff --git a/scheme/nvidia-coserv/README.md b/store-plugin/nvidia-coserv/README.md similarity index 100% rename from scheme/nvidia-coserv/README.md rename to store-plugin/nvidia-coserv/README.md diff --git a/scheme/nvidia-coserv/coserv_handler.go b/store-plugin/nvidia-coserv/coserv_handler.go similarity index 82% rename from scheme/nvidia-coserv/coserv_handler.go rename to store-plugin/nvidia-coserv/coserv_handler.go index 2f501cf2..03bdc860 100644 --- a/scheme/nvidia-coserv/coserv_handler.go +++ b/store-plugin/nvidia-coserv/coserv_handler.go @@ -10,13 +10,17 @@ import ( "fmt" "io" "net/http" + "slices" "time" "github.com/veraison/cmw" "github.com/veraison/corim/comid" "github.com/veraison/corim/corim" "github.com/veraison/corim/coserv" + "github.com/veraison/services/handler" + "github.com/veraison/services/log" "github.com/veraison/services/plugin" + vtscoserv "github.com/veraison/services/vts/coserv" ) // ----- JSON Helper Types ----- @@ -31,13 +35,22 @@ type RimServiceResponse struct { RequestId string `json:"request_id"` } -type CoservProxyHandler struct{} +type CoservProxyHandler struct { + *vtscoserv.StoreConfig +} -var ( - dummyAuthority = []byte{0xab, 0xcd, 0xef} -) +func (s *CoservProxyHandler) Init(params *plugin.Parameters) error { + var cfg vtscoserv.StoreConfig + if err := (&cfg).FromParams(params); err != nil { + log.Named(SchemeName).Warn("CoSERV configuration not passed") + s.StoreConfig = nil + } else { + s.StoreConfig = &cfg + } + return nil +} -func (s CoservProxyHandler) Init(*plugin.Parameters) error { +func (s CoservProxyHandler) Fini() error { return nil } @@ -161,14 +174,11 @@ func (s CoservProxyHandler) addReferenceValuesForClass(query *coserv.Query, c *c // We'll just look at reference value triples in the CoMID for _, triple := range c.Triples.ReferenceValues.Values { + authority := s.Authority // Turn each triple into a quad - // TODO(paulhowardarm) - This authority is a dummy value. + // TODO(paulhowardarm) // We need some kind of cert here, representing this plug-in's authority to re-package from NVIDIA CoRIM // We probably also need an NVIDIA cert in the chain - authority, err := comid.NewCryptoKeyTaggedBytes(dummyAuthority) - if err != nil { - return fmt.Errorf("failed to make authority tagged bytes: %w", err) - } rvQuad := coserv.RefValQuad{ Authorities: comid.NewCryptoKeys().Add(authority), @@ -184,7 +194,15 @@ func (s CoservProxyHandler) addReferenceValuesForClass(query *coserv.Query, c *c return nil } -func (s CoservProxyHandler) GetEndorsements(tenantID string, query string) ([]byte, error) { +func (s CoservProxyHandler) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + if s.StoreConfig == nil { + panic("received CoSERV request when CoSERV API is disabled") + } + + if !slices.Contains(SupportedCoservProfiles, profile) { + return nil, handler.ErrUnsupported + } + var q coserv.Coserv if err := q.FromBase64Url(query); err != nil { return nil, err @@ -221,8 +239,7 @@ func (s CoservProxyHandler) GetEndorsements(tenantID string, query string) ([]by } } - // Set expiry on the results - fairly arbitrary expiry time of 1 hour - coservResult.SetExpiry(time.Now().Add(time.Hour)) + coservResult.SetExpiry(time.Now().Add(s.MaxExpiry)) // Add all results into the top-level CoSERV object err := q.AddResults(coservResult) @@ -230,5 +247,17 @@ func (s CoservProxyHandler) GetEndorsements(tenantID string, query string) ([]by return nil, err } - return q.ToCBOR() + return &q, nil +} + +func (o CoservProxyHandler) GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) { + return nil, handler.ErrUnsupported +} + +func (o CoservProxyHandler) GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) { + return nil, handler.ErrUnsupported +} + +func (b CoservProxyHandler) AddCorimBytes(data []byte, scheme string, activate bool) error { + return handler.ErrUnsupported } diff --git a/scheme/nvidia-coserv/plugin/Makefile b/store-plugin/nvidia-coserv/plugin/Makefile similarity index 63% rename from scheme/nvidia-coserv/plugin/Makefile rename to store-plugin/nvidia-coserv/plugin/Makefile index 41edc8c8..8200e1aa 100644 --- a/scheme/nvidia-coserv/plugin/Makefile +++ b/store-plugin/nvidia-coserv/plugin/Makefile @@ -1,8 +1,8 @@ -# Copyright 2025 Contributors to the Veraison project. +# Copyright 2025-2026 Contributors to the Veraison project. # SPDX-License-Identifier: Apache-2.0 PLUGIN := ../../bin/coserv-nvidia.plugin -GOPKG := github.com/veraison/services/scheme/nvidia-coserv +GOPKG := github.com/veraison/services/store-plugin/nvidia-coserv SRCS := main.go include ../../../mk/common.mk diff --git a/scheme/amd-kds-coserv/plugin/main.go b/store-plugin/nvidia-coserv/plugin/main.go similarity index 64% rename from scheme/amd-kds-coserv/plugin/main.go rename to store-plugin/nvidia-coserv/plugin/main.go index 5f0bd720..26afb981 100644 --- a/scheme/amd-kds-coserv/plugin/main.go +++ b/store-plugin/nvidia-coserv/plugin/main.go @@ -4,12 +4,12 @@ package main import ( + scheme "github.com/veraison/services/store-plugin/nvidia-coserv" "github.com/veraison/services/handler" "github.com/veraison/services/plugin" - scheme "github.com/veraison/services/scheme/amd-kds-coserv" ) func main() { - handler.RegisterCoservProxyHandler(&scheme.CoservProxyHandler{}) + handler.RegisterEndorsementStore(&scheme.CoservProxyHandler{}) plugin.Serve() } diff --git a/scheme/nvidia-coserv/scheme.go b/store-plugin/nvidia-coserv/scheme.go similarity index 60% rename from scheme/nvidia-coserv/scheme.go rename to store-plugin/nvidia-coserv/scheme.go index cbc5a7da..68c6d2b9 100644 --- a/scheme/nvidia-coserv/scheme.go +++ b/store-plugin/nvidia-coserv/scheme.go @@ -1,4 +1,4 @@ -// Copyright 2025 Contributors to the Veraison project. +// Copyright 2025-2026 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package nvidiacoserv @@ -9,4 +9,5 @@ var ( CoservMediaTypes = []string{ `application/coserv+cbor; profile="tag:github.com/veraison,2023:nvidia_coserv_proxy#1.0.0"`, } + SupportedCoservProfiles = []string{"tag:github.com/veraison,2023:nvidia_coserv_proxy#1.0.0"} ) diff --git a/vts/Makefile b/vts/Makefile index 3727b75f..b0f50ad4 100644 --- a/vts/Makefile +++ b/vts/Makefile @@ -4,6 +4,7 @@ SUBDIR += trustedservices SUBDIR += policymanager SUBDIR += cmd/vts-service +SUBDIR += endorsementstore # Create directories for packaging (TODO: May be a better way to do this) install: diff --git a/vts/cmd/vts-service/config-docker.yaml b/vts/cmd/vts-service/config-docker.yaml index 0e976972..7d873cc7 100644 --- a/vts/cmd/vts-service/config-docker.yaml +++ b/vts/cmd/vts-service/config-docker.yaml @@ -3,12 +3,10 @@ plugin: #backend: builtin backend: go-plugin - go-plugin: + go-plugin-schemes: dir: ../../../scheme/bin/ -store: - dbms: sqlite3 - dsn: file:/veraison/stores/vts/store.sql - trace-sql: true + go-plugin-stores: + dir: ../../../store-plugin/bin/ po-store: backend: sql sql: @@ -26,10 +24,18 @@ vts: ear-signer: alg: ES256 key: ./skey.jwk -coserv: - max-expiry: 5 mins - signer: - alg: ES256 - key: ./skey.jwk +endorsement-store: + coserv: + signer: + alg: ES256 + key: ./skey.jwk + max-expiry: 5 mins + active-plugins: + - corim-store + plugin-parameters: + corim-store: + dbms: sqlite3 + dsn: file::memory:?cache=shared + trace-sql: false logging: level: debug diff --git a/vts/cmd/vts-service/config.yaml b/vts/cmd/vts-service/config.yaml index dd9d2ca1..80cbf7b0 100644 --- a/vts/cmd/vts-service/config.yaml +++ b/vts/cmd/vts-service/config.yaml @@ -3,17 +3,15 @@ # automatically if run via ./run-vts script. plugin: backend: go-plugin - go-plugin: + go-plugin-schemes: dir: ../../../scheme/bin/ + go-plugin-stores: + dir: ../../../store-plugin/bin/ po-store: backend: sql sql: driver: sqlite3 datasource: /tmp/po-store.sql -store: - dbms: sqlite3 - dsn: file::memory:?cache=shared - trace-sql: false po-agent: backend: opa vts: @@ -28,11 +26,19 @@ vts: ear-signer: alg: ES256 key: ./skey.jwk -coserv: - max-expiry: 5 mins - signer: - alg: ES256 - key: ./skey.jwk +endorsement-store: + coserv: + signer: + alg: ES256 + key: ./skey.jwk + max-expiry: 5 mins + active-plugins: + - corim-store + plugin-parameters: + corim-store: + dbms: sqlite3 + dsn: file::memory:?cache=shared + trace-sql: false logging: level: debug # Scheme configuration Example diff --git a/vts/cmd/vts-service/main.go b/vts/cmd/vts-service/main.go index 3d1ce5e4..d46f41eb 100644 --- a/vts/cmd/vts-service/main.go +++ b/vts/cmd/vts-service/main.go @@ -18,8 +18,8 @@ import ( "github.com/veraison/services/policy" "github.com/veraison/services/vts/coserv" "github.com/veraison/services/vts/earsigner" + "github.com/veraison/services/vts/endorsementstore" "github.com/veraison/services/vts/policymanager" - "github.com/veraison/services/vts/store" "github.com/veraison/services/vts/trustedservices" ) @@ -31,8 +31,8 @@ func main() { log.Fatalf("could not read config: %v", err) } - subs, err := config.GetSubs(v, "store", "po-store", - "*po-agent", "plugin", "*vts", "ear-signer", "*coserv", "*logging", "*scheme") + subs, err := config.GetSubs(v, "po-store", "endorsement-store", + "*po-agent", "plugin", "*vts", "ear-signer", "*logging", "*scheme") if err != nil { log.Fatal(err) } @@ -42,12 +42,32 @@ func main() { log.Fatalf("could not configure logging: %v", err) } - log.Info("initializing stores") - enStore, err := store.New(subs["store"], log.Named("store")) + log.Info("processing endorsement store configuration") + endorsementStoreCfg := subs["endorsement-store"] + activeStorePlugins := endorsementStoreCfg.GetStringSlice("active-plugins") + if len(activeStorePlugins) == 0 { + log.Fatalf("could not find active plugins list in endorsement store config") + } + var ( + coservContext *coserv.Context + coservCfg *coserv.StoreConfig + ) + if cfg := endorsementStoreCfg.Sub("coserv"); cfg != nil { + coservContext, err = coserv.NewCoservContextFromViper(cfg) + if err != nil { + log.Fatalf("CoSERV config initialization failed: %v", err) + } + coservCfg = &coservContext.StoreConfig + } else { + log.Info("No CoSERV config supplied") + } + endorsementStorePluginConfig, err := endorsementstore.CreateStoreParams( + endorsementStoreCfg, coservCfg) if err != nil { - log.Fatalf("endorsement store initialization failed: %v", err) + log.Fatalf("failed to load endorsement store parameter: %v", err) } + log.Info("initializing policy store") poStore, err := policy.NewStore(subs["po-store"], log.Named("po-store")) if err != nil { log.Fatalf("policy store initialization failed: %v", err) @@ -61,65 +81,81 @@ func main() { log.Info("loading attestation schemes") var schemePluginManager plugin.IManager[handler.ISchemeHandler] - var coservProxyPluginManager plugin.IManager[handler.ICoservProxyHandler] + var endorsementStoreManager plugin.IManager[handler.IEndorsementStorePlugin] log.Debug("loading scheme configuration") - pluginConfig, err := plugin.ParametersMapFromViper(subs["scheme"], handler.PluginNameFromScheme) + schemePluginConfig, err := plugin.ParametersMapFromViper(subs["scheme"], handler.PluginNameFromScheme) if err != nil { log.Fatalf("could not load scheme config: %v", err) } - log.Debug("loading scheme plugins") - psubs, err := config.GetSubs(subs["plugin"], "*go-plugin", "*builtin") + log.Debug("loading scheme and endorsement store plugins") + psubs, err := config.GetSubs(subs["plugin"], "*go-plugin-schemes", "*builtin", "go-plugin-stores") if err != nil { log.Fatalf("could not get subs: %v", err) } switch config.SchemeLoader { case "plugins": - loader, err := plugin.CreateGoPluginLoader( - psubs["go-plugin"].AllSettings(), - pluginConfig, + schemeLoader, err := plugin.CreateGoPluginLoader( + psubs["go-plugin-schemes"].AllSettings(), + schemePluginConfig, log.Named("plugin")) if err != nil { log.Fatalf("could not create plugin loader: %v", err) } + storeLoader, err := plugin.CreateGoPluginLoader( + psubs["go-plugin-stores"].AllSettings(), + endorsementStorePluginConfig, + log.Named("store-loader")) + if err != nil { + log.Fatalf("could not create store plugin loader: %v", err) + } schemePluginManager, err = plugin.CreateGoPluginManagerWithLoader( - loader, + schemeLoader, "scheme-handler", log.Named("plugin"), handler.SchemeHandlerRPC) if err != nil { log.Fatalf("could not create store PluginManagerWithLoader: %v", err) } - coservProxyPluginManager, err = plugin.CreateGoPluginManagerWithLoader( - loader, - "coserv-proxy-handler", - log.Named("plugin"), - handler.CoservProxyHandlerRPC) + endorsementStoreManager, err = plugin.CreateGoPluginManagerWithLoader( + storeLoader, + "endorsement-store", + log.Named("store-plugin-manager"), + handler.EndorsementStoreRPC) if err != nil { - log.Fatalf("could not create coserv PluginManagerWithLoader: %v", err) + log.Fatalf("could not create store PluginManagerWithLoader: %v", err) } case "builtin": - loader, err := builtin.CreateBuiltinLoader( + schemeLoader, err := builtin.CreateBuiltinLoader( + psubs["builtin"].AllSettings(), + builtin.SchemePlugin, + schemePluginConfig, + log.Named("builtin-scheme")) + if err != nil { + log.Fatalf("could not create builtin scheme loader: %v", err) + } + storeLoader, err := builtin.CreateBuiltinLoader( psubs["builtin"].AllSettings(), - pluginConfig, - log.Named("builtin")) + builtin.StorePlugin, + endorsementStorePluginConfig, + log.Named("builtin-store")) if err != nil { - log.Fatalf("could not create builtin loader: %v", err) + log.Fatalf("could not create builtin store loader: %v", err) } schemePluginManager, err = builtin.CreateBuiltinManagerWithLoader[handler.ISchemeHandler]( - loader, log.Named("builtin"), + schemeLoader, log.Named("builtin"), "scheme-handler") if err != nil { log.Fatalf("could not create store BuiltinManagerWithLoader: %v", err) } - coservProxyPluginManager, err = builtin.CreateBuiltinManagerWithLoader[handler.ICoservProxyHandler]( - loader, log.Named("builtin"), - "coserv-handler") + endorsementStoreManager, err = builtin.CreateBuiltinManagerWithLoader[handler.IEndorsementStorePlugin]( + storeLoader, log.Named("builtin"), + "endorsement-store") if err != nil { - log.Fatalf("could not create coserv BuiltinManagerWithLoader: %v", err) + log.Fatalf("could not create endorsement-store BuiltinManagerWithLoader: %v", err) } default: log.Panicw("invalid SchemeLoader value", "SchemeLoader", config.SchemeLoader) @@ -135,36 +171,20 @@ func main() { log.Info("\t", mt) } - log.Info("CoSERV Proxy media types:") - for _, mt := range coservProxyPluginManager.GetRegisteredMediaTypes() { - log.Info("\t", mt) - } - log.Info("loading EAR signer") earSigner, err := earsigner.New(subs["ear-signer"], afero.NewOsFs()) if err != nil { log.Fatalf("EAR signer initialization failed: %v", err) } - var coservContext *coserv.Context - if subs["coserv"].IsSet("signer") { - coservContext, err = coserv.NewCoservContextFromViper(subs["coserv"]) - if err != nil { - log.Fatal("CoSERV config initialization: %v", err) - } - - // CoSERV media types. - log.Info("TODO CoSERV profile types:") - } - log.Info("initializing service") // from this point onwards taStore, enStore, evPluginManager, // endPluginManager, storePluginManager, coservProxyPluginManager, // policyManager and earSigner are owned by vts - vts := trustedservices.NewGRPC(enStore, - schemePluginManager, coservProxyPluginManager, - policyManager, earSigner, coservContext, log.Named("vts")) - + vts := trustedservices.NewGRPC( + schemePluginManager, endorsementStoreManager, + policyManager, earSigner, coservContext, activeStorePlugins, + log.Named("vts")) if err = vts.Init(subs["vts"]); err != nil { log.Fatalf("VTS initialisation failed: %v", err) } diff --git a/vts/coserv/context.go b/vts/coserv/context.go index 1d574c3e..5313d264 100644 --- a/vts/coserv/context.go +++ b/vts/coserv/context.go @@ -11,16 +11,14 @@ import ( "github.com/spf13/afero" "github.com/spf13/viper" - "github.com/veraison/corim/comid" "github.com/veraison/services/config" ) var FallbackMaxExpiry = 5 * time.Minute type Context struct { - Signer ISigner - FallbackAuthority *comid.CryptoKey - MaxExpiry time.Duration + StoreConfig + Signer ISigner } func NewCoservContextFromViper(v *viper.Viper) (*Context, error) { @@ -44,10 +42,11 @@ func NewCoservContextFromViper(v *viper.Viper) (*Context, error) { return nil, fmt.Errorf("fallback authority: %w", err) } + storeCfg := CreateStoreConfig(authority, expiry) + return &Context{ - Signer: signer, - MaxExpiry: expiry, - FallbackAuthority: authority, + StoreConfig: storeCfg, + Signer: signer, }, nil } diff --git a/vts/coserv/store_config.go b/vts/coserv/store_config.go new file mode 100644 index 00000000..f38d74a2 --- /dev/null +++ b/vts/coserv/store_config.go @@ -0,0 +1,69 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package coserv + +import ( + "time" + + "github.com/veraison/corim/comid" + "github.com/veraison/services/plugin" +) + +// StoreConfig contains the CoSERV related config parameters +// that the endorsement store uses. +type StoreConfig struct { + Authority *comid.CryptoKey + MaxExpiry time.Duration +} + +func CreateStoreConfig(auth *comid.CryptoKey, exp time.Duration) StoreConfig { + return StoreConfig{ + auth, + exp, + } +} + +// private constants used for serializing and deserializing +// StoreConfig +var ( + fallbackAuthorityKey = "coserv-fallback-authority" + maxExpiryKey = "coserv-max-expiry" +) + +// Deserialize plugin.Parameters into StoreConfig +func (o *StoreConfig) FromParams(params *plugin.Parameters) error { + var key comid.CryptoKey + + auth, err := params.GetBytes(fallbackAuthorityKey) + if err != nil { + return err + } + + exp, err := params.GetInt64(maxExpiryKey) + if err != nil { + return err + } + + if err := (&key).UnmarshalCBOR(auth); err != nil { + return err + } + + o.Authority = &key + o.MaxExpiry = time.Duration(exp) + return nil +} + +// Serialize StoreConfig to plugin.Parameters +func (o *StoreConfig) ToParams() (*plugin.Parameters, error) { + auth, err := o.Authority.MarshalCBOR() + if err != nil { + return nil, err + } + exp := o.MaxExpiry.Nanoseconds() + + params := plugin.NewParameters(). + SetBytes(fallbackAuthorityKey, auth). + SetInt64(maxExpiryKey, exp) + + return params, nil +} diff --git a/vts/coserv/store_config_test.go b/vts/coserv/store_config_test.go new file mode 100644 index 00000000..0b6b6e85 --- /dev/null +++ b/vts/coserv/store_config_test.go @@ -0,0 +1,92 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package coserv + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/veraison/corim/comid" + "github.com/veraison/services/plugin" +) + +func TestConfigStoreConfigFrom(t *testing.T) { + params1 := plugin.NewParameters() + params1.SetBytes(fallbackAuthorityKey, cryptoKeyCBOR()) + params1.SetInt64(maxExpiryKey, expiryInt()) + + params2 := plugin.NewParameters() + + params3 := plugin.NewParameters() + params3.SetBytes(fallbackAuthorityKey, cryptoKeyCBOR()) + + params4 := plugin.NewParameters() + params4.SetBytes(fallbackAuthorityKey, []byte{0xa2}) // invalid CBOR + params4.SetInt64(maxExpiryKey, expiryInt()) + + storePar1 := new(StoreConfig) + err := storePar1.FromParams(params1) + assert.NoError(t, err) + assert.Equal(t, storePar1.MaxExpiry, expiry()) + assert.Equal(t, *cryptoKey(), *storePar1.Authority) + + storePar2 := new(StoreConfig) + err = storePar2.FromParams(params2) + assert.Error(t, err) + + storePar3 := new(StoreConfig) + err = storePar3.FromParams(params3) + assert.Error(t, err) + + storePar4 := new(StoreConfig) + err = storePar4.FromParams(params4) + assert.Error(t, err) +} + +func TestConfigStoreConfigTo(t *testing.T) { + cfg := StoreConfig{ + cryptoKey(), + expiry(), + } + + params, err := (&cfg).ToParams() + + assert.NoError(t, err) + + exp, err := params.GetInt64(maxExpiryKey) + assert.NoError(t, err) + assert.Equal(t, exp, expiryInt()) + + auth, err := params.GetBytes(fallbackAuthorityKey) + assert.NoError(t, err) + assert.Equal(t, auth, cryptoKeyCBOR()) +} + +func cryptoKey() *comid.CryptoKey { + k, err := comid.NewCryptoKeyTaggedBytes([]byte{0, 0, 0, 0}) + if err != nil { + panic(err) + } + return k +} + +func cryptoKeyCBOR() []byte { + bytes, err := cryptoKey().MarshalCBOR() + if err != nil { + panic(err) + } + return bytes +} + +func expiryInt() int64 { + return expiry().Nanoseconds() +} + +func expiry() time.Duration { + d, err := time.ParseDuration("300s") + if err != nil { + panic(err) + } + return d +} diff --git a/vts/endorsementstore/Makefile b/vts/endorsementstore/Makefile new file mode 100644 index 00000000..b7926a1a --- /dev/null +++ b/vts/endorsementstore/Makefile @@ -0,0 +1,13 @@ +# Copyright 2026 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +INTERFACES := ../../handler/iendorsementstore.go + +MOCKPKG := mocks + +lint-hook-pre: _mocks + +include ../../mk/common.mk +include ../../mk/pkg.mk +include ../../mk/lint.mk +include ../../mk/test.mk diff --git a/vts/endorsementstore/Readme.md b/vts/endorsementstore/Readme.md new file mode 100644 index 00000000..5790a478 --- /dev/null +++ b/vts/endorsementstore/Readme.md @@ -0,0 +1,49 @@ +# VTS Endorsement Store + +The endorsement store used by VTS consists of a list of store plugins. The +implementation iterates over the list for each store operation, until it +succeeds. + +## Configuration +The endorsement store configuration is passed in the `endorsement-store` stanza +of the configuration yaml file. + +Example: +```yaml +endorsement-store: + coserv: + signer: + alg: ES256 + key: ./skey.jwk + max-expiry: 5 mins + active-plugins: + - corim-store + plugin-parameters: + corim-store: + dbms: sqlite3 + dsn: file::memory:?cache=shared + trace-sql: false +``` + +* `coserv`: contains the CoSERV configuration parameters of the store. These +parameters are passed along with the plugin parameters to each of the plugins +during plugin initialization. + +* `active-plugins`: contains the list of store plugins that will be used to +construct the endorsement-store. The plugins will be iterated over in the +order they are given in the config. + +* `plugin-parameters`: contains parameters for store plugins. + +### Notes + +* If the `active-stores` list contains a plugin name which is not loadable, +the service will fail to start. + +* If the `active-stores` section is empty or not present, the service will +fail to start. + +* **Important**: For submitting endorsements, only the first store in the list +is used, instead of iterating over the list of active stores. If a read-only +store is used as the first store in the list, endorsement provisioning will not +work. diff --git a/vts/endorsementstore/config.go b/vts/endorsementstore/config.go new file mode 100644 index 00000000..a34a1e2b --- /dev/null +++ b/vts/endorsementstore/config.go @@ -0,0 +1,73 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package endorsementstore + +import ( + "fmt" + + "github.com/spf13/viper" + "github.com/veraison/services/plugin" + vtscoserv "github.com/veraison/services/vts/coserv" +) + +// Creates the plugin parameters map by combining the plugin specific +// parameters with the common CoSERV configuration +func CreateStoreParams( + cfg *viper.Viper, + coservCfg *vtscoserv.StoreConfig, +) (map[string]*plugin.Parameters, error) { + var ( + err error + coservParams *plugin.Parameters + pluginParams map[string]*plugin.Parameters + ) + if cfg == nil { + // caller should ensure that the config is non-nil + panic("empty endorsement store configuration") + } + if cfg.Sub("coserv") != nil && coservCfg == nil { + // plugins can assume that CoSERV API is disabled if CoSERV + // configuration is not passed to them + panic("invalid endorsement-store configuration for CoSERV") + } + + activePlugins := cfg.GetStringSlice("active-plugins") + if len(activePlugins) == 0 { + return nil, fmt.Errorf("failed to read active store plugins list") + } + + if p := cfg.Sub("plugin-parameters"); p != nil { + pluginParams, err = plugin.ParametersMapFromViper(p, nil) + if err != nil { + return nil, fmt.Errorf("failed to read store plugin parameters: %w", err) + } + } + + if coservCfg == nil { + // no CoSERV parameters to broadcast + return pluginParams, nil + } + + coservParams, err = coservCfg.ToParams() + if err != nil { + return nil, fmt.Errorf("invalid CoSERV parameters: %w", err) + } + + if pluginParams == nil { + pluginParams = make(map[string]*plugin.Parameters, len(activePlugins)) + } + + // broadcast coserv config to all plugins + for _, s := range activePlugins { + // create parameter maps for plugins that do not have + // config parameters in their plugin configuration + if _, ok := pluginParams[s]; !ok { + pluginParams[s] = plugin.NewParameters() + } + if err := pluginParams[s].Merge(coservParams); err != nil { + return nil, fmt.Errorf("failed to create store config: %w", err) + } + } + + return pluginParams, nil +} diff --git a/vts/endorsementstore/config_test.go b/vts/endorsementstore/config_test.go new file mode 100644 index 00000000..a9aca774 --- /dev/null +++ b/vts/endorsementstore/config_test.go @@ -0,0 +1,129 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package endorsementstore + +import ( + "maps" + "slices" + "testing" + "time" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/veraison/corim/comid" + "github.com/veraison/services/plugin" + "github.com/veraison/services/vts/coserv" +) + +func TestCreateStoreParams(t *testing.T) { + cfg1 := viper.New() + cfg1.Set("coserv", coservSub()) + cfg1.Set("active-plugins", activePluginsSub()) + cfg1.Set("plugin-parameters", pluginParamsSub()) + + cfg2 := viper.New() + cfg2.Set("active-plugins", activePluginsSub()) + cfg2.Set("plugin-parameters", pluginParamsSub()) + + cfg3 := viper.New() + cfg3.Set("active-plugins", activePluginsSub()) + + cfg4 := viper.New() + cfg4.Set("coserv", coservSub()) + cfg4.Set("plugin-parameters", pluginParamsSub()) + + cfg5 := viper.New() + cfg5.Set("active-plugins", coservSub()) + + cfg6 := viper.New() + cfg6.Set("active-plugins", activePluginsSub()) + cfg6.Set("plugin-parameters", coservSub()) + + cfg7 := viper.New() + cfg7.Set("active-plugins", activePluginsSub()) + cfg7.Set("coserv", coservSub()) + + coservCfg := &coserv.StoreConfig{ + Authority: cryptoKey(), + MaxExpiry: expiry(), + } + + assert.Panics(t, func() { CreateStoreParams(cfg1, nil) }) //nolint:errcheck + assert.Panics(t, func() { CreateStoreParams(nil, coservCfg) }) //nolint:errcheck + + var ( + pm map[string]*plugin.Parameters + err error + ) + pm, err = CreateStoreParams(cfg1, coservCfg) + + assert.NoError(t, err) + assert.ElementsMatch(t, slices.Collect(maps.Keys(pm)), storeList()) + assert.Equal(t, pm["pl1"].MustGetString("pl1-cf1"), "val1") + + for _, cfg := range pm { + var storeCfg coserv.StoreConfig + assert.NoError(t, (&storeCfg).FromParams(cfg)) + assert.Equal(t, storeCfg.Authority, cryptoKey()) + assert.Equal(t, storeCfg.MaxExpiry, expiry()) + } + + _, err = CreateStoreParams(cfg2, coservCfg) + assert.NoError(t, err) + + _, err = CreateStoreParams(cfg3, coservCfg) + assert.NoError(t, err) + + _, err = CreateStoreParams(cfg5, coservCfg) + assert.Error(t, err) + + _, err = CreateStoreParams(cfg6, coservCfg) + assert.Error(t, err) + + _, err = CreateStoreParams(cfg7, coservCfg) + assert.NoError(t, err) +} + +func coservSub() map[string]any { + m := map[string]any{ + "signer": map[string]any{ + "alg": "xx", + "key": "yy", + }, + "max-expiry": "1m", + } + return m +} + +func activePluginsSub() []any { + return []any{"pl1", "pl2"} +} + +func pluginParamsSub() map[string]any { + return map[string]any{ + "pl1": map[string]any{ + "pl1-cf1": "val1", + "pl1-cf2": "val2", + }, + } +} + +func cryptoKey() *comid.CryptoKey { + k, err := comid.NewCryptoKeyTaggedBytes([]byte{0, 0, 0, 0}) + if err != nil { + panic(err) + } + return k +} + +func expiry() time.Duration { + d, err := time.ParseDuration("300s") + if err != nil { + panic(err) + } + return d +} + +func storeList() []string { + return []string{"pl1", "pl2"} +} diff --git a/vts/endorsementstore/mocks/iendorsementstore.go b/vts/endorsementstore/mocks/iendorsementstore.go new file mode 100644 index 00000000..7108af42 --- /dev/null +++ b/vts/endorsementstore/mocks/iendorsementstore.go @@ -0,0 +1,248 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../../handler/iendorsementstore.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + comid "github.com/veraison/corim/comid" + coserv "github.com/veraison/corim/coserv" + plugin "github.com/veraison/services/plugin" +) + +// MockIEndorsementStorePlugin is a mock of IEndorsementStorePlugin interface. +type MockIEndorsementStorePlugin struct { + ctrl *gomock.Controller + recorder *MockIEndorsementStorePluginMockRecorder +} + +// MockIEndorsementStorePluginMockRecorder is the mock recorder for MockIEndorsementStorePlugin. +type MockIEndorsementStorePluginMockRecorder struct { + mock *MockIEndorsementStorePlugin +} + +// NewMockIEndorsementStorePlugin creates a new mock instance. +func NewMockIEndorsementStorePlugin(ctrl *gomock.Controller) *MockIEndorsementStorePlugin { + mock := &MockIEndorsementStorePlugin{ctrl: ctrl} + mock.recorder = &MockIEndorsementStorePluginMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIEndorsementStorePlugin) EXPECT() *MockIEndorsementStorePluginMockRecorder { + return m.recorder +} + +// AddCorimBytes mocks base method. +func (m *MockIEndorsementStorePlugin) AddCorimBytes(data []byte, scheme string, activate bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCorimBytes", data, scheme, activate) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddCorimBytes indicates an expected call of AddCorimBytes. +func (mr *MockIEndorsementStorePluginMockRecorder) AddCorimBytes(data, scheme, activate interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCorimBytes", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).AddCorimBytes), data, scheme, activate) +} + +// ExecuteCoservQuery mocks base method. +func (m *MockIEndorsementStorePlugin) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteCoservQuery", profile, query) + ret0, _ := ret[0].(*coserv.Coserv) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteCoservQuery indicates an expected call of ExecuteCoservQuery. +func (mr *MockIEndorsementStorePluginMockRecorder) ExecuteCoservQuery(profile, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteCoservQuery", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).ExecuteCoservQuery), profile, query) +} + +// Fini mocks base method. +func (m *MockIEndorsementStorePlugin) Fini() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fini") + ret0, _ := ret[0].(error) + return ret0 +} + +// Fini indicates an expected call of Fini. +func (mr *MockIEndorsementStorePluginMockRecorder) Fini() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fini", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).Fini)) +} + +// GetAttestationScheme mocks base method. +func (m *MockIEndorsementStorePlugin) GetAttestationScheme() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAttestationScheme") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetAttestationScheme indicates an expected call of GetAttestationScheme. +func (mr *MockIEndorsementStorePluginMockRecorder) GetAttestationScheme() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttestationScheme", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).GetAttestationScheme)) +} + +// GetKeyTriples mocks base method. +func (m *MockIEndorsementStorePlugin) GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKeyTriples", env, scheme, exact) + ret0, _ := ret[0].([]*comid.KeyTriple) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKeyTriples indicates an expected call of GetKeyTriples. +func (mr *MockIEndorsementStorePluginMockRecorder) GetKeyTriples(env, scheme, exact interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyTriples", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).GetKeyTriples), env, scheme, exact) +} + +// GetName mocks base method. +func (m *MockIEndorsementStorePlugin) GetName() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetName") + ret0, _ := ret[0].(string) + return ret0 +} + +// GetName indicates an expected call of GetName. +func (mr *MockIEndorsementStorePluginMockRecorder) GetName() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetName", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).GetName)) +} + +// GetSupportedMediaTypes mocks base method. +func (m *MockIEndorsementStorePlugin) GetSupportedMediaTypes() map[string][]string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSupportedMediaTypes") + ret0, _ := ret[0].(map[string][]string) + return ret0 +} + +// GetSupportedMediaTypes indicates an expected call of GetSupportedMediaTypes. +func (mr *MockIEndorsementStorePluginMockRecorder) GetSupportedMediaTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupportedMediaTypes", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).GetSupportedMediaTypes)) +} + +// GetValueTriples mocks base method. +func (m *MockIEndorsementStorePlugin) GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValueTriples", env, scheme, exact) + ret0, _ := ret[0].([]*comid.ValueTriple) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValueTriples indicates an expected call of GetValueTriples. +func (mr *MockIEndorsementStorePluginMockRecorder) GetValueTriples(env, scheme, exact interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValueTriples", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).GetValueTriples), env, scheme, exact) +} + +// Init mocks base method. +func (m *MockIEndorsementStorePlugin) Init(params *plugin.Parameters) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", params) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockIEndorsementStorePluginMockRecorder) Init(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIEndorsementStorePlugin)(nil).Init), params) +} + +// MockIEndorsementStore is a mock of IEndorsementStore interface. +type MockIEndorsementStore struct { + ctrl *gomock.Controller + recorder *MockIEndorsementStoreMockRecorder +} + +// MockIEndorsementStoreMockRecorder is the mock recorder for MockIEndorsementStore. +type MockIEndorsementStoreMockRecorder struct { + mock *MockIEndorsementStore +} + +// NewMockIEndorsementStore creates a new mock instance. +func NewMockIEndorsementStore(ctrl *gomock.Controller) *MockIEndorsementStore { + mock := &MockIEndorsementStore{ctrl: ctrl} + mock.recorder = &MockIEndorsementStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIEndorsementStore) EXPECT() *MockIEndorsementStoreMockRecorder { + return m.recorder +} + +// AddCorimBytes mocks base method. +func (m *MockIEndorsementStore) AddCorimBytes(data []byte, scheme string, activate bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddCorimBytes", data, scheme, activate) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddCorimBytes indicates an expected call of AddCorimBytes. +func (mr *MockIEndorsementStoreMockRecorder) AddCorimBytes(data, scheme, activate interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddCorimBytes", reflect.TypeOf((*MockIEndorsementStore)(nil).AddCorimBytes), data, scheme, activate) +} + +// ExecuteCoservQuery mocks base method. +func (m *MockIEndorsementStore) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteCoservQuery", profile, query) + ret0, _ := ret[0].(*coserv.Coserv) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteCoservQuery indicates an expected call of ExecuteCoservQuery. +func (mr *MockIEndorsementStoreMockRecorder) ExecuteCoservQuery(profile, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteCoservQuery", reflect.TypeOf((*MockIEndorsementStore)(nil).ExecuteCoservQuery), profile, query) +} + +// GetKeyTriples mocks base method. +func (m *MockIEndorsementStore) GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKeyTriples", env, scheme, exact) + ret0, _ := ret[0].([]*comid.KeyTriple) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKeyTriples indicates an expected call of GetKeyTriples. +func (mr *MockIEndorsementStoreMockRecorder) GetKeyTriples(env, scheme, exact interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyTriples", reflect.TypeOf((*MockIEndorsementStore)(nil).GetKeyTriples), env, scheme, exact) +} + +// GetValueTriples mocks base method. +func (m *MockIEndorsementStore) GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetValueTriples", env, scheme, exact) + ret0, _ := ret[0].([]*comid.ValueTriple) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetValueTriples indicates an expected call of GetValueTriples. +func (mr *MockIEndorsementStoreMockRecorder) GetValueTriples(env, scheme, exact interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetValueTriples", reflect.TypeOf((*MockIEndorsementStore)(nil).GetValueTriples), env, scheme, exact) +} diff --git a/vts/endorsementstore/store.go b/vts/endorsementstore/store.go new file mode 100644 index 00000000..b1c3b02c --- /dev/null +++ b/vts/endorsementstore/store.go @@ -0,0 +1,171 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package endorsementstore + +import ( + "errors" + + "github.com/veraison/corim/comid" + "github.com/veraison/corim/coserv" + "github.com/veraison/services/handler" + "github.com/veraison/services/plugin" + "go.uber.org/zap" +) + +type StoreManager = plugin.IManager[handler.IEndorsementStorePlugin] + +var ErrNoStores = errors.New("no store plugins active") + +type VtsEndorsementStore struct { + logger *zap.SugaredLogger + stores []handler.IEndorsementStorePlugin +} + +func (s *VtsEndorsementStore) addStore(store handler.IEndorsementStorePlugin) { + if s.stores == nil { + s.stores = []handler.IEndorsementStorePlugin{store} + } else { + s.stores = append(s.stores, store) + } +} + +func (s *VtsEndorsementStore) storeList() ([]handler.IEndorsementStorePlugin, error) { + if len(s.stores) == 0 { + return nil, ErrNoStores + } + return s.stores, nil +} + +func CreateEndorsementStore(pluginNames []string, manager StoreManager, logger *zap.SugaredLogger) (handler.IEndorsementStore, error) { + if len(pluginNames) == 0 { + err := errors.New("no names in `active-stores` list") + return nil, err + } + store := VtsEndorsementStore{ + logger: logger, + } + for _, pl := range pluginNames { + st, err := manager.LookupByName(pl) + if err != nil { + logger.Errorf("failed to load store `%s': %v", pl, err) + return nil, err + } + store.addStore(st) + } + return &store, nil +} + +func (s *VtsEndorsementStore) GetKeyTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.KeyTriple, error) { + s.logger.Debugw("searching key triples for", "env", env) + errs := make(map[error]bool, 2) // keep track of kind of errors encountered + stores, err := s.storeList() + if err != nil { + s.logger.Errorf("failed to fetch store list: %v", err) + return nil, err + } + for _, store := range stores { + name := store.GetName() + t, err := store.GetKeyTriples(env, scheme, exact) + if err != nil { + if logOrErr(err, s.logger, name, &errs) != nil { + return nil, err + } + } else { + s.logger.Infof("found key triples in store `%s'", name) + return t, nil + } + } + return nil, computeStoreErr(errs) +} + +func (s *VtsEndorsementStore) GetValueTriples(env *comid.Environment, scheme string, exact bool) ([]*comid.ValueTriple, error) { + s.logger.Debugw("searching value triples for", "env", env) + errs := make(map[error]bool, 2) // keep track of kind of errors encountered + stores, err := s.storeList() + if err != nil { + s.logger.Errorf("failed to fetch store list: %v", err) + return nil, err + } + for _, store := range stores { + name := store.GetName() + t, err := store.GetValueTriples(env, scheme, exact) + if err != nil { + if logOrErr(err, s.logger, name, &errs) != nil { + return nil, err + } + } else { + s.logger.Infof("found value triples in store `%s'", name) + return t, nil + } + } + return nil, computeStoreErr(errs) +} + +func (s *VtsEndorsementStore) ExecuteCoservQuery(profile, query string) (*coserv.Coserv, error) { + s.logger.Debugw("coserv query", "profile", profile, "query", query) + errs := make(map[error]bool, 2) // keep track of kind of errors encountered + stores, err := s.storeList() + if err != nil { + s.logger.Errorf("failed to fetch store list: %v", err) + return nil, err + } + for _, store := range stores { + name := store.GetName() + res, err := store.ExecuteCoservQuery(profile, query) + if err != nil { + if logOrErr(err, s.logger, name, &errs) != nil { + return nil, err + } + } else { + s.logger.Infof("CoSERV results found in store `%s'", name) + return res, nil + } + } + return nil, computeStoreErr(errs) +} + +func (s *VtsEndorsementStore) AddCorimBytes(data []byte, scheme string, activate bool) error { + // only submit endorsements to the first store in list + stores, err := s.storeList() + if err != nil { + s.logger.Errorf("failed to fetch store list: %v", err) + return err + } + store := stores[0] // stores is guaranteed to contain at least one entry + name := store.GetName() + if err := store.AddCorimBytes(data, scheme, activate); err != nil { + s.logger.Errorf("failed to add endorsements to store `%s': %v", name, err) + return err + } + return nil +} + +func logOrErr(err error, logger *zap.SugaredLogger, name string, errs *map[error]bool) error { + if err != nil { + // errors.Is can be used because the plugin rpc client parses the error + // before returning. + if errors.Is(err, handler.ErrNotFound) { + (*errs)[handler.ErrNotFound] = true + logger.Debugf("not found in store `%s'", name) + return nil + } + if errors.Is(err, handler.ErrUnsupported) { + (*errs)[handler.ErrUnsupported] = true + logger.Debugf("store `%s' does not support operation", name) + return nil + } + logger.Errorf("Failed to fetch from store `%s': %v", name, err) + return err + } // else + return nil +} + +func computeStoreErr(errs map[error]bool) error { + // if ErrNotFound is encountered at least once, it means there + // is at least one store that supports the operation. + if _, ok := errs[handler.ErrNotFound]; ok { + return handler.ErrNotFound + } + // got ErrUnsupported from all stores + return handler.ErrUnsupported +} diff --git a/vts/endorsementstore/store_test.go b/vts/endorsementstore/store_test.go new file mode 100644 index 00000000..05e2c354 --- /dev/null +++ b/vts/endorsementstore/store_test.go @@ -0,0 +1,339 @@ +// Copyright 2026 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package endorsementstore + +import ( + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/veraison/corim/comid" + "github.com/veraison/corim/coserv" + "github.com/veraison/services/handler" + "github.com/veraison/services/log" + mockstore "github.com/veraison/services/vts/endorsementstore/mocks" +) + +var ( + errUnsupp = fmt.Errorf("error: %w", handler.ErrUnsupported) + errNotFound = fmt.Errorf("error: %w", handler.ErrNotFound) + errUnexpected = fmt.Errorf("unexpected error") +) + +func TestGetKeyTriples(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + storeNotFound := notFoundStore(t, ctrl) + storeUnsupported := unsupportedStore(t, ctrl) + storeUnexpected := unexpStore(t, ctrl) + storeValid := validStore(t, ctrl) + + store0 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + + store1 := VtsEndorsementStore{ + log.Named("store1"), + nil, + } + store1.addStore(storeNotFound) + store1.addStore(storeUnsupported) + store1.addStore(storeNotFound) + + store2 := VtsEndorsementStore{ + log.Named("store2"), + []handler.IEndorsementStorePlugin{}, + } + store2.addStore(storeUnsupported) + store2.addStore(storeUnsupported) + + store3 := VtsEndorsementStore{ + log.Named("store3"), + []handler.IEndorsementStorePlugin{}, + } + store3.addStore(storeNotFound) + store3.addStore(storeUnsupported) + store3.addStore(storeUnexpected) + + store4 := VtsEndorsementStore{ + log.Named("store4"), + []handler.IEndorsementStorePlugin{}, + } + store4.addStore(storeNotFound) + store4.addStore(storeUnsupported) + store4.addStore(storeValid) + + _, e0 := store0.GetKeyTriples(nil, "", false) + _, e1 := store1.GetKeyTriples(nil, "", false) + _, e2 := store2.GetKeyTriples(nil, "", false) + _, e3 := store3.GetKeyTriples(nil, "", false) + _, e4 := store4.GetKeyTriples(nil, "", false) + + assert.ErrorIs(t, e0, ErrNoStores) + assert.ErrorIs(t, e1, handler.ErrNotFound, "expected ENOTFOUND") + assert.ErrorIs(t, e2, handler.ErrUnsupported, "expected EUNSUPPORTED") + assert.ErrorContains(t, e3, errUnexpected.Error()) + assert.NoError(t, e4) +} + +func TestGetValueTriples(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + storeNotFound := notFoundStore(t, ctrl) + storeUnsupported := unsupportedStore(t, ctrl) + storeUnexpected := unexpStore(t, ctrl) + storeValid := validStore(t, ctrl) + + store0 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + + store1 := VtsEndorsementStore{ + log.Named("store1"), + []handler.IEndorsementStorePlugin{}, + } + store1.addStore(storeNotFound) + store1.addStore(storeUnsupported) + store1.addStore(storeNotFound) + + store2 := VtsEndorsementStore{ + log.Named("store2"), + []handler.IEndorsementStorePlugin{}, + } + store2.addStore(storeUnsupported) + store2.addStore(storeUnsupported) + + store3 := VtsEndorsementStore{ + log.Named("store3"), + []handler.IEndorsementStorePlugin{}, + } + store3.addStore(storeNotFound) + store3.addStore(storeUnsupported) + store3.addStore(storeUnexpected) + + store4 := VtsEndorsementStore{ + log.Named("store4"), + []handler.IEndorsementStorePlugin{}, + } + store4.addStore(storeNotFound) + store4.addStore(storeUnsupported) + store4.addStore(storeValid) + + _, e0 := store0.GetValueTriples(nil, "", false) + _, e1 := store1.GetValueTriples(nil, "", false) + _, e2 := store2.GetValueTriples(nil, "", false) + _, e3 := store3.GetValueTriples(nil, "", false) + _, e4 := store4.GetValueTriples(nil, "", false) + + assert.ErrorIs(t, e0, ErrNoStores) + assert.ErrorIs(t, e1, handler.ErrNotFound, "expected ENOTFOUND") + assert.ErrorIs(t, e2, handler.ErrUnsupported, "expected EUNSUPPORTED") + assert.ErrorContains(t, e3, errUnexpected.Error()) + assert.NoError(t, e4) +} + +func TestExecuteCoservQuery(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + storeNotFound := notFoundStore(t, ctrl) + storeUnsupported := unsupportedStore(t, ctrl) + storeUnexpected := unexpStore(t, ctrl) + storeValid := validStore(t, ctrl) + + store0 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + + store1 := VtsEndorsementStore{ + log.Named("store1"), + []handler.IEndorsementStorePlugin{}, + } + store1.addStore(storeNotFound) + store1.addStore(storeUnsupported) + store1.addStore(storeNotFound) + + store2 := VtsEndorsementStore{ + log.Named("store2"), + []handler.IEndorsementStorePlugin{}, + } + store2.addStore(storeUnsupported) + store2.addStore(storeUnsupported) + + store3 := VtsEndorsementStore{ + log.Named("store3"), + []handler.IEndorsementStorePlugin{}, + } + store3.addStore(storeNotFound) + store3.addStore(storeUnsupported) + store3.addStore(storeUnexpected) + + store4 := VtsEndorsementStore{ + log.Named("store4"), + []handler.IEndorsementStorePlugin{}, + } + store4.addStore(storeNotFound) + store4.addStore(storeUnsupported) + store4.addStore(storeValid) + + _, e0 := store0.ExecuteCoservQuery("", "") + _, e1 := store1.ExecuteCoservQuery("", "") + _, e2 := store2.ExecuteCoservQuery("", "") + _, e3 := store3.ExecuteCoservQuery("", "") + _, e4 := store4.ExecuteCoservQuery("", "") + + assert.ErrorIs(t, e0, ErrNoStores) + assert.ErrorIs(t, e1, handler.ErrNotFound, "expected ENOTFOUND") + assert.ErrorIs(t, e2, handler.ErrUnsupported, "expected EUNSUPPORTED") + assert.ErrorContains(t, e3, errUnexpected.Error()) + assert.NoError(t, e4) +} + +func TestAddCorimBytes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + storeUnexpected := unexpStore(t, ctrl) + storeValid := validStore(t, ctrl) + + store0 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + + store1 := VtsEndorsementStore{ + log.Named("store1"), + []handler.IEndorsementStorePlugin{}, + } + store1.addStore(storeUnexpected) + store1.addStore(storeValid) + + store2 := VtsEndorsementStore{ + log.Named("store2"), + []handler.IEndorsementStorePlugin{}, + } + store2.addStore(storeValid) + store2.addStore(storeUnexpected) + + e0 := store0.AddCorimBytes(make([]byte, 0), "", false) + e1 := store1.AddCorimBytes(make([]byte, 0), "", false) + e2 := store2.AddCorimBytes(make([]byte, 0), "", false) + + assert.ErrorIs(t, e0, ErrNoStores) + assert.Error(t, e1) + assert.NoError(t, e2) +} + +func TestComputeStoreErr(t *testing.T) { + m := make(map[error]bool, 2) + m[handler.ErrUnsupported] = true + assert.ErrorIs(t, computeStoreErr(m), handler.ErrUnsupported) + + m[handler.ErrNotFound] = true + assert.ErrorIs(t, computeStoreErr(m), handler.ErrNotFound) +} + +func TestLogOrErr(t *testing.T) { + var res error + + res = logOrErr(handler.ErrNotFound, log.Named("temp"), "", &map[error]bool{}) + assert.NoError(t, res) + + res = logOrErr(handler.ErrUnsupported, log.Named("temp"), "", &map[error]bool{}) + assert.NoError(t, res) + + res = logOrErr(errUnexpected, log.Named("temp"), "", &map[error]bool{}) + assert.ErrorIs(t, res, errUnexpected) +} + +func TestStoreList(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + store0 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + + store1 := VtsEndorsementStore{ + log.Named("store0"), + []handler.IEndorsementStorePlugin{}, + } + store1.addStore(validStore(t, ctrl)) + + _, e0 := store0.storeList() + _, e1 := store1.storeList() + + assert.ErrorIs(t, e0, ErrNoStores) + assert.NoError(t, e1) +} + +func notFoundStore(t *testing.T, ctrl *gomock.Controller) *mockstore.MockIEndorsementStorePlugin { + return errStore(t, ctrl, errNotFound) +} + +func unsupportedStore(t *testing.T, ctrl *gomock.Controller) *mockstore.MockIEndorsementStorePlugin { + return errStore(t, ctrl, errUnsupp) +} + +func unexpStore(t *testing.T, ctrl *gomock.Controller) *mockstore.MockIEndorsementStorePlugin { + return errStore(t, ctrl, errUnexpected) +} + +func errStore(t *testing.T, ctrl *gomock.Controller, err error) *mockstore.MockIEndorsementStorePlugin { + storeUnexpected := mockstore.NewMockIEndorsementStorePlugin(ctrl) + storeUnexpected.EXPECT(). + GetKeyTriples(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(nil, err) + storeUnexpected.EXPECT(). + GetValueTriples(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(nil, err) + storeUnexpected.EXPECT(). + ExecuteCoservQuery(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(nil, err) + storeUnexpected.EXPECT(). + AddCorimBytes(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(err) + storeUnexpected.EXPECT(). + GetName(). + AnyTimes(). + Return("error-store") + + return storeUnexpected +} + +func validStore(t *testing.T, ctrl *gomock.Controller) *mockstore.MockIEndorsementStorePlugin { + storeValid := mockstore.NewMockIEndorsementStorePlugin(ctrl) + storeValid.EXPECT(). + GetKeyTriples(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return([]*comid.KeyTriple{&comid.KeyTriple{}}, nil) + storeValid.EXPECT(). + GetValueTriples(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return([]*comid.ValueTriple{&comid.ValueTriple{}}, nil) + storeValid.EXPECT(). + ExecuteCoservQuery(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(&coserv.Coserv{}, nil) + storeValid.EXPECT(). + AddCorimBytes(gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes(). + Return(nil) + storeValid.EXPECT(). + GetName(). + AnyTimes(). + Return("valid-store") + + return storeValid +} diff --git a/vts/trustedservices/trustedservices_grpc.go b/vts/trustedservices/trustedservices_grpc.go index 96b7c4fe..040338f0 100644 --- a/vts/trustedservices/trustedservices_grpc.go +++ b/vts/trustedservices/trustedservices_grpc.go @@ -22,7 +22,6 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/structpb" - corimstore "github.com/veraison/corim-store/pkg/store" "github.com/veraison/corim/comid" "github.com/veraison/corim/corim" "github.com/veraison/corim/coserv" @@ -34,6 +33,7 @@ import ( "github.com/veraison/services/vts/appraisal" vtscoserv "github.com/veraison/services/vts/coserv" "github.com/veraison/services/vts/earsigner" + "github.com/veraison/services/vts/endorsementstore" "github.com/veraison/services/vts/policymanager" ) @@ -68,13 +68,14 @@ func NewGRPCConfig() *GRPCConfig { type GRPC struct { ServerAddress string - Store *corimstore.Store - SchemePluginManager plugin.IManager[handlermod.ISchemeHandler] - CoservProxyPluginManager plugin.IManager[handlermod.ICoservProxyHandler] - PolicyManager *policymanager.PolicyManager - EarSigner earsigner.IEarSigner - CoservContext *vtscoserv.Context - rootCerts *x509.CertPool + SchemePluginManager plugin.IManager[handlermod.ISchemeHandler] + StoreManager plugin.IManager[handlermod.IEndorsementStorePlugin] + PolicyManager *policymanager.PolicyManager + EarSigner earsigner.IEarSigner + CoservContext *vtscoserv.Context + rootCerts *x509.CertPool + endorsementStore handlermod.IEndorsementStore + activeStorePlugins []string Server *grpc.Server Socket net.Listener @@ -85,22 +86,22 @@ type GRPC struct { } func NewGRPC( - store *corimstore.Store, schemePluginManager plugin.IManager[handlermod.ISchemeHandler], - coservProxyPluginManager plugin.IManager[handlermod.ICoservProxyHandler], + storeManager plugin.IManager[handlermod.IEndorsementStorePlugin], policyManager *policymanager.PolicyManager, earSigner earsigner.IEarSigner, coservConfig *vtscoserv.Context, + activeStorePlugins []string, logger *zap.SugaredLogger, ) ITrustedServices { return &GRPC{ - Store: store, - SchemePluginManager: schemePluginManager, - CoservProxyPluginManager: coservProxyPluginManager, - PolicyManager: policyManager, - EarSigner: earSigner, - CoservContext: coservConfig, - logger: logger, + SchemePluginManager: schemePluginManager, + StoreManager: storeManager, + PolicyManager: policyManager, + EarSigner: earSigner, + CoservContext: coservConfig, + activeStorePlugins: activeStorePlugins, + logger: logger, } } @@ -128,6 +129,11 @@ func (o *GRPC) Init( return err } + if err := o.initStore(); err != nil { + o.logger.Errorf("failed to initialize active stores: %v", err) + return err + } + if cfg.ListenAddress != "" { o.ServerAddress = cfg.ListenAddress } else { @@ -167,6 +173,15 @@ func (o *GRPC) Init( return nil } +func (o *GRPC) initStore() error { + store, err := endorsementstore.CreateEndorsementStore(o.activeStorePlugins, o.StoreManager, o.logger) + if err != nil { + return err + } + o.endorsementStore = store + return nil +} + func (o *GRPC) Close() error { if o.Server != nil { o.Server.GracefulStop() @@ -176,12 +191,8 @@ func (o *GRPC) Close() error { o.logger.Errorf("scheme plugin manager shutdown failed: %v", err) } - if err := o.CoservProxyPluginManager.Close(); err != nil { - o.logger.Errorf("coserv plugin manager shutdown failed: %v", err) - } - - if err := o.Store.Close(); err != nil { - o.logger.Errorf("store closure failed: %v", err) + if err := o.StoreManager.Close(); err != nil { + o.logger.Errorf("store plugin manager shutdown failed: %v", err) } if err := o.EarSigner.Close(); err != nil { @@ -271,9 +282,8 @@ func (o *GRPC) SubmitEndorsements( } else if !resp.IsValid { return submitEndorsementErrorResponse(resp.Error()), nil } - label := fmt.Sprintf("%s/%s", DummyTenantID, handlerPlugin.GetAttestationScheme()) - if err := o.Store.AddBytes(req.Data, label, true); err != nil { + if err := o.endorsementStore.AddCorimBytes(req.Data, label, true); err != nil { return submitEndorsementErrorResponse(err), nil } @@ -407,14 +417,21 @@ func (o *GRPC) GetAttestation( // we are forced to do inexact matching here for now, and leave // it to the attestation schemes to resolve this. matchExactly := false - trustAnchors, err := o.getKeyTriples(appraisal.TrustAnchorIDs, appraisal.StoreLabel(), matchExactly) + var trustAnchors []*comid.KeyTriple + trustAnchors, err = o.getKeyTriples(appraisal.TrustAnchorIDs, appraisal.StoreLabel(), matchExactly) + if err != nil { - if errors.Is(err, corimstore.ErrNoMatch) { - err = handlermod.BadEvidence("no trust anchor for %s", appraisal.DescribeTrustAnchorIDs()) - appraisal.SetAllClaims(ear.CryptoValidationFailedClaim) - appraisal.AddPolicyClaim("problem", "no trust anchor for evidence") + if errors.Is(err, handlermod.ErrNotFound) { + // finalize will be called in the next block + o.logger.Warn("no trust anchor in store") + } else { + return o.finalize(appraisal, err) } - + } + if trustAnchors == nil { + err = handlermod.BadEvidence("no trust anchor for %s", appraisal.DescribeTrustAnchorIDs()) + appraisal.SetAllClaims(ear.CryptoValidationFailedClaim) + appraisal.AddPolicyClaim("problem", "no trust anchor for evidence") return o.finalize(appraisal, err) } @@ -436,10 +453,15 @@ func (o *GRPC) GetAttestation( "software-id", appraisal.ReferenceValueIDs, "trust-anchor-id", appraisal.TrustAnchorIDs) + var endorsements []*comid.ValueTriple o.logger.Debug("obtaining endorsements...") - endorsements, err := o.getValueTriples(appraisal.ReferenceValueIDs, appraisal.StoreLabel(), true) + endorsements, err = o.getValueTriples(appraisal.ReferenceValueIDs, appraisal.StoreLabel(), true) if err != nil { - return o.finalize(appraisal, err) + if errors.Is(err, handlermod.ErrNotFound) { + o.logger.Warn("no reference values in store") + } else { + return o.finalize(appraisal, err) + } } o.logger.Debug("validating evidence...") @@ -484,17 +506,22 @@ func (o *GRPC) getKeyTriples( label string, exact bool, ) ([]*comid.KeyTriple, error) { + var keyTriples []*comid.KeyTriple //nolint for _, taID := range trustAnchorIDs { - triples, err := o.Store.GetActiveKeyTriples(taID, label, exact) + triples, err := o.endorsementStore.GetKeyTriples(taID, label, exact) if err != nil { + o.logger.Warnw("could not find in store", "taID", taID, "error", err) return nil, err } - keyTriples = append(keyTriples, triples...) } + if len(keyTriples) == 0 { + return nil, handlermod.ErrNotFound + } + return keyTriples, nil } @@ -503,17 +530,22 @@ func (o *GRPC) getValueTriples( label string, exact bool, ) ([]*comid.ValueTriple, error) { + var valueTriples []*comid.ValueTriple //nolint for _, valID := range referenceValueIDs { - triples, err := o.Store.GetActiveValueTriples(valID, label, exact) - if err != nil && !errors.Is(err, corimstore.ErrNoMatch) { + triples, err := o.endorsementStore.GetValueTriples(valID, label, exact) + if err != nil && !errors.Is(err, handlermod.ErrNotFound) { + o.logger.Warnw("could not find in store", "valID", valID, "error", err) return nil, err } - valueTriples = append(valueTriples, triples...) } + if len(valueTriples) == 0 { + return nil, handlermod.ErrNotFound + } + return valueTriples, nil } @@ -558,8 +590,11 @@ func (c *GRPC) GetSupportedCoservMediaTypes(context.Context, *emptypb.Empty) (*p "application/rim+cbor", ) + // FIXME(dhanus): Implement a way of obtaining the supported + // CoSERV profiles from the store plugins. The MediaType associated + // with store plugins are temporarily used until then. coservProxyDerived := c.assembleCoservMediaTypes( - c.CoservProxyPluginManager.GetRegisteredMediaTypes(), + c.StoreManager.GetRegisteredMediaTypes(), "application/coserv+cbor", ) @@ -632,29 +667,29 @@ func getEndorsementsError(err error) *proto.EndorsementQueryOut { } } -func (o *GRPC) getEndorsementsFromStores(queryIn *proto.EndorsementQueryIn) ([]byte, error) { +func (o *GRPC) getEndorsementsFromStores( + queryIn *proto.EndorsementQueryIn, +) ([]byte, error) { var query coserv.Coserv if err := query.FromBase64Url(queryIn.Query); err != nil { return nil, err } - coservService := corimstore.NewCoSERVService( - o.Store, - o.CoservContext.FallbackAuthority, - o.CoservContext.MaxExpiry, - ) - if err := coservService.UpdateCoSERV(&query); err != nil { + _, mtParams, err := mime.ParseMediaType(queryIn.MediaType) + if err != nil { + o.logger.Warnf("Bad request: could not parse media type: %v", err) return nil, err } + profile := mtParams["profile"] - return query.ToCBOR() -} + resp, err := o.endorsementStore.ExecuteCoservQuery(profile, queryIn.Query) + if err != nil { + o.logger.Infof("could not find coserv result in store: %v", err) + } else { + return resp.ToCBOR() + } -func (o *GRPC) getEndorsementsFromProxy( - handlerPlugin handlermod.ICoservProxyHandler, - query *proto.EndorsementQueryIn, -) ([]byte, error) { - return handlerPlugin.GetEndorsements(DummyTenantID, query.Query) + return nil, handlermod.ErrNotFound } func (o *GRPC) GetEndorsements( @@ -664,20 +699,11 @@ func (o *GRPC) GetEndorsements( o.logger.Debugw("GetEndorsements", "media-type", query.MediaType) var ( - err error - out []byte - handlerPlugin handlermod.ICoservProxyHandler + err error + out []byte ) - // First, check to see if we have a CoSERV proxy plugin that can handle this query - handlerPlugin, err = o.CoservProxyPluginManager.LookupByMediaType(query.MediaType) - if err == nil { - // No error means we have a proxy plugin, so delegate to that. - out, err = o.getEndorsementsFromProxy(handlerPlugin, query) - } else { - // There was no proxy plugin, so assume we can obtain from own stores - out, err = o.getEndorsementsFromStores(query) - } + out, err = o.getEndorsementsFromStores(query) if err != nil { return getEndorsementsError(err), nil