diff --git a/docs/get_delete_api_implementation_guide.md b/docs/get_delete_api_implementation_guide.md new file mode 100644 index 00000000..b13e7e1e --- /dev/null +++ b/docs/get_delete_api_implementation_guide.md @@ -0,0 +1,182 @@ +# GET & DELETE APIs for Veraison Provisioning Interface + +## Summary + +This implementation adds GET and DELETE functionality to the Veraison Provisioning Interface, allowing users to: +1. **Retrieve endorsements** (reference values and trust anchors) that have been submitted +2. **Delete endorsements** when they are no longer needed + +Previously, the interface only supported submitting endorsements via POST. + +## Implementation Details + +### 1. Protocol Buffer Definitions + +Added new message types and RPC methods to `proto/vts.proto`: + +- **GetEndorsementsRequest**: Request to retrieve endorsements with optional filtering + - `key_prefix`: Filter by key prefix (optional) + - `endorsement_type`: Type filter ("trust-anchor", "reference-value", or "all") + +- **GetEndorsementsResponse**: Response containing matching endorsements + - `endorsements`: List of endorsement entries + - `status`: Operation status + +- **DeleteEndorsementsRequest**: Request to delete endorsements + - `key`: Key or key prefix to delete (required) + - `endorsement_type`: Type filter ("trust-anchor", "reference-value", or "all") + +- **DeleteEndorsementsResponse**: Response with deletion results + - `deleted_count`: Number of endorsements deleted + - `status`: Operation status + +### 2. VTS Service Layer + +**File**: `vts/trustedservices/trustedservices_grpc.go` + +Implemented two new gRPC methods: + +- **GetEndorsements**: Retrieves endorsements from trust anchor and/or reference value stores + - Supports filtering by key prefix + - Supports filtering by endorsement type + - Returns all matching endorsements with their keys and values + +- **DeleteEndorsements**: Deletes endorsements from stores + - Supports exact key match or prefix-based deletion + - Supports filtering by endorsement type + - Returns count of deleted entries + +### 3. Provisioner Layer + +**Files**: +- `provisioning/provisioner/iprovisioner.go` +- `provisioning/provisioner/provisioner.go` + +Added two new methods to the provisioner interface and implementation that call the VTS gRPC methods. + +### 4. Provisioning API Layer + +**Files**: +- `provisioning/api/handler.go` +- `provisioning/api/router.go` + +Added two new REST API endpoints: + +#### GET /endorsement-provisioning/v1/endorsements + +Query parameters: +- `key-prefix` (optional): Filter endorsements by key prefix +- `type` (optional): Filter by type ("all", "trust-anchor", "reference-value"). Default: "all" + +Response: JSON with endorsements list + +Example: +```bash +curl -H "Accept: application/json" \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?type=trust-anchor" +``` + +#### DELETE /endorsement-provisioning/v1/endorsements + +Query parameters: +- `key` (required): Key or key prefix to delete +- `type` (optional): Filter by type ("all", "trust-anchor", "reference-value"). Default: "all" + +Response: JSON with deletion count + +Example: +```bash +curl -X DELETE \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?key=my-endorsement-key" +``` + +### 5. VTS Client Layer + +**File**: `vtsclient/vtsclient_grpc.go` + +Added wrapper methods in the GRPC client to call the new VTS service methods. + +### 6. Tests + +**File**: `provisioning/api/handler_test.go` + +Added comprehensive unit tests: +- `TestHandler_GetEndorsements_Success`: Test successful retrieval +- `TestHandler_GetEndorsements_WithFilter`: Test with filtering +- `TestHandler_GetEndorsements_InvalidType`: Test invalid type parameter +- `TestHandler_DeleteEndorsements_Success`: Test successful deletion +- `TestHandler_DeleteEndorsements_MissingKey`: Test missing key parameter +- `TestHandler_DeleteEndorsements_Error`: Test error handling + +All tests pass successfully. + +## API Usage Examples + +### Retrieve All Endorsements + +```bash +curl -H "Accept: application/json" \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements" +``` + +### Retrieve Trust Anchors Only + +```bash +curl -H "Accept: application/json" \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?type=trust-anchor" +``` + +### Retrieve Endorsements with Key Prefix + +```bash +curl -H "Accept: application/json" \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?key-prefix=arm-cca" +``` + +### Delete Specific Endorsement + +```bash +curl -X DELETE \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?key=my-key" +``` + +### Delete All Trust Anchors with Prefix + +```bash +curl -X DELETE \ + "http://localhost:8888/endorsement-provisioning/v1/endorsements?key=prefix-&type=trust-anchor" +``` + +## Security Considerations + +- All new endpoints are protected by the same authorization middleware as the existing submit endpoint +- Users must have the `ProvisionerRole` to access these endpoints +- Deletion is permanent and cannot be undone +- Key prefix matching allows batch deletion - use with caution + +## Files Modified + +1. `proto/vts.proto` - Proto definitions +2. `proto/vts_grpc.pb.go` - Generated gRPC code (manually updated) +3. `proto/endorsement_query.go` - New proto message structures +4. `vts/trustedservices/trustedservices_grpc.go` - VTS service implementation +5. `provisioning/provisioner/iprovisioner.go` - Provisioner interface +6. `provisioning/provisioner/provisioner.go` - Provisioner implementation +7. `provisioning/api/handler.go` - API handlers +8. `provisioning/api/router.go` - Route registration +9. `provisioning/api/handler_test.go` - Unit tests +10. `provisioning/api/mocks/iprovisioner.go` - Mock updates +11. `vtsclient/vtsclient_grpc.go` - VTS client wrapper + +## Testing + +All unit tests pass: +```bash +cd provisioning/api && go test -v +``` + +Build verification: +```bash +go build ./provisioning/... +go build ./vts/... +``` diff --git a/proto/endorsement_query.go b/proto/endorsement_query.go new file mode 100644 index 00000000..66e23a0d --- /dev/null +++ b/proto/endorsement_query.go @@ -0,0 +1,37 @@ +// Copyright 2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package proto + +// GetEndorsementsRequest is the request message for GetEndorsements RPC +type GetEndorsementsRequest struct { + // Optional key prefix to filter endorsements. If empty, returns all endorsements. + KeyPrefix string `json:"key_prefix,omitempty"` + // Type of endorsement to retrieve: "trust-anchor", "reference-value", or "all" + EndorsementType string `json:"endorsement_type,omitempty"` +} + +// EndorsementEntry represents a single endorsement entry +type EndorsementEntry struct { + Key string `json:"key"` + Values []string `json:"values"` +} + +// GetEndorsementsResponse is the response message for GetEndorsements RPC +type GetEndorsementsResponse struct { + Endorsements []*EndorsementEntry `json:"endorsements"` + Status *Status `json:"status"` +} + +// DeleteEndorsementsRequest is the request message for DeleteEndorsements RPC +type DeleteEndorsementsRequest struct { + // Key or key prefix to delete + Key string `json:"key"` + // Type of endorsement to delete: "trust-anchor", "reference-value", or "all" + EndorsementType string `json:"endorsement_type,omitempty"` +} + +// DeleteEndorsementsResponse is the response message for DeleteEndorsements RPC +type DeleteEndorsementsResponse struct { + DeletedCount int32 `json:"deleted_count"` + Status *Status `json:"status"` +} diff --git a/proto/vts.proto b/proto/vts.proto index b860b864..f7ed6174 100644 --- a/proto/vts.proto +++ b/proto/vts.proto @@ -35,6 +35,35 @@ message PublicKey { string key = 1; } +message GetEndorsementsRequest { + // Optional key prefix to filter endorsements. If empty, returns all endorsements. + string key_prefix = 1; + // Type of endorsement to retrieve: "trust-anchor", "reference-value", or "all" + string endorsement_type = 2; +} + +message EndorsementEntry { + string key = 1; + repeated string values = 2; +} + +message GetEndorsementsResponse { + repeated EndorsementEntry endorsements = 1; + Status status = 2; +} + +message DeleteEndorsementsRequest { + // Key or key prefix to delete + string key = 1; + // Type of endorsement to delete: "trust-anchor", "reference-value", or "all" + string endorsement_type = 2; +} + +message DeleteEndorsementsResponse { + int32 deleted_count = 1; + Status status = 2; +} + // Client interface for the Veraison Trusted Services component. // protolint:disable MAX_LINE_LENGTH service VTS { @@ -49,6 +78,12 @@ service VTS { rpc GetSupportedProvisioningMediaTypes(google.protobuf.Empty) returns (MediaTypeList); rpc SubmitEndorsements(SubmitEndorsementsRequest) returns (SubmitEndorsementsResponse); + // Returns endorsements (trust anchors and/or reference values) based on the provided filter + rpc GetEndorsements(GetEndorsementsRequest) returns (GetEndorsementsResponse); + + // Deletes endorsements (trust anchors and/or reference values) based on the provided key + rpc DeleteEndorsements(DeleteEndorsementsRequest) returns (DeleteEndorsementsResponse); + // Returns the public key used to sign evidence. rpc GetEARSigningPublicKey(google.protobuf.Empty) returns (PublicKey); } diff --git a/proto/vts_grpc.pb.go b/proto/vts_grpc.pb.go index fdc60431..4311285d 100644 --- a/proto/vts_grpc.pb.go +++ b/proto/vts_grpc.pb.go @@ -8,6 +8,7 @@ package proto import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" @@ -31,6 +32,10 @@ type VTSClient interface { GetSupportedVerificationMediaTypes(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MediaTypeList, error) GetSupportedProvisioningMediaTypes(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*MediaTypeList, error) SubmitEndorsements(ctx context.Context, in *SubmitEndorsementsRequest, opts ...grpc.CallOption) (*SubmitEndorsementsResponse, error) + // Returns endorsements (trust anchors and/or reference values) based on the provided filter + GetEndorsements(ctx context.Context, in *GetEndorsementsRequest, opts ...grpc.CallOption) (*GetEndorsementsResponse, error) + // Deletes endorsements (trust anchors and/or reference values) based on the provided key + DeleteEndorsements(ctx context.Context, in *DeleteEndorsementsRequest, opts ...grpc.CallOption) (*DeleteEndorsementsResponse, error) // Returns the public key used to sign evidence. GetEARSigningPublicKey(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PublicKey, error) } @@ -88,6 +93,24 @@ func (c *vTSClient) SubmitEndorsements(ctx context.Context, in *SubmitEndorsemen return out, nil } +func (c *vTSClient) GetEndorsements(ctx context.Context, in *GetEndorsementsRequest, opts ...grpc.CallOption) (*GetEndorsementsResponse, error) { + out := new(GetEndorsementsResponse) + err := c.cc.Invoke(ctx, "/proto.VTS/GetEndorsements", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *vTSClient) DeleteEndorsements(ctx context.Context, in *DeleteEndorsementsRequest, opts ...grpc.CallOption) (*DeleteEndorsementsResponse, error) { + out := new(DeleteEndorsementsResponse) + err := c.cc.Invoke(ctx, "/proto.VTS/DeleteEndorsements", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *vTSClient) GetEARSigningPublicKey(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*PublicKey, error) { out := new(PublicKey) err := c.cc.Invoke(ctx, "/proto.VTS/GetEARSigningPublicKey", in, out, opts...) @@ -109,6 +132,10 @@ type VTSServer interface { GetSupportedVerificationMediaTypes(context.Context, *emptypb.Empty) (*MediaTypeList, error) GetSupportedProvisioningMediaTypes(context.Context, *emptypb.Empty) (*MediaTypeList, error) SubmitEndorsements(context.Context, *SubmitEndorsementsRequest) (*SubmitEndorsementsResponse, error) + // Returns endorsements (trust anchors and/or reference values) based on the provided filter + GetEndorsements(context.Context, *GetEndorsementsRequest) (*GetEndorsementsResponse, error) + // Deletes endorsements (trust anchors and/or reference values) based on the provided key + DeleteEndorsements(context.Context, *DeleteEndorsementsRequest) (*DeleteEndorsementsResponse, error) // Returns the public key used to sign evidence. GetEARSigningPublicKey(context.Context, *emptypb.Empty) (*PublicKey, error) mustEmbedUnimplementedVTSServer() @@ -133,6 +160,12 @@ func (UnimplementedVTSServer) GetSupportedProvisioningMediaTypes(context.Context func (UnimplementedVTSServer) SubmitEndorsements(context.Context, *SubmitEndorsementsRequest) (*SubmitEndorsementsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SubmitEndorsements not implemented") } +func (UnimplementedVTSServer) GetEndorsements(context.Context, *GetEndorsementsRequest) (*GetEndorsementsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetEndorsements not implemented") +} +func (UnimplementedVTSServer) DeleteEndorsements(context.Context, *DeleteEndorsementsRequest) (*DeleteEndorsementsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteEndorsements not implemented") +} func (UnimplementedVTSServer) GetEARSigningPublicKey(context.Context, *emptypb.Empty) (*PublicKey, error) { return nil, status.Errorf(codes.Unimplemented, "method GetEARSigningPublicKey not implemented") } @@ -239,6 +272,42 @@ func _VTS_SubmitEndorsements_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _VTS_GetEndorsements_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetEndorsementsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VTSServer).GetEndorsements(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.VTS/GetEndorsements", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VTSServer).GetEndorsements(ctx, req.(*GetEndorsementsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _VTS_DeleteEndorsements_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteEndorsementsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VTSServer).DeleteEndorsements(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/proto.VTS/DeleteEndorsements", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VTSServer).DeleteEndorsements(ctx, req.(*DeleteEndorsementsRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _VTS_GetEARSigningPublicKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(emptypb.Empty) if err := dec(in); err != nil { @@ -284,6 +353,14 @@ var VTS_ServiceDesc = grpc.ServiceDesc{ MethodName: "SubmitEndorsements", Handler: _VTS_SubmitEndorsements_Handler, }, + { + MethodName: "GetEndorsements", + Handler: _VTS_GetEndorsements_Handler, + }, + { + MethodName: "DeleteEndorsements", + Handler: _VTS_DeleteEndorsements_Handler, + }, { MethodName: "GetEARSigningPublicKey", Handler: _VTS_GetEARSigningPublicKey_Handler, diff --git a/provisioning/api/handler.go b/provisioning/api/handler.go index 0b9090ca..79c8bc0c 100644 --- a/provisioning/api/handler.go +++ b/provisioning/api/handler.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package api @@ -23,6 +23,8 @@ var ( type IHandler interface { Submit(c *gin.Context) GetWellKnownProvisioningInfo(c *gin.Context) + GetEndorsements(c *gin.Context) + DeleteEndorsements(c *gin.Context) } type Handler struct { @@ -219,3 +221,111 @@ func (o *Handler) GetWellKnownProvisioningInfo(c *gin.Context) { c.Header("Content-Type", capability.WellKnownMediaType) c.JSON(http.StatusOK, obj) } + +func (o *Handler) GetEndorsements(c *gin.Context) { + // Get query parameters + keyPrefix := c.Query("key-prefix") + endorsementType := c.Query("type") + + // Default to "all" if type is not specified + if endorsementType == "" { + endorsementType = "all" + } + + // Validate endorsement type + validTypes := map[string]bool{ + "all": true, + "trust-anchor": true, + "reference-value": true, + } + if !validTypes[endorsementType] { + ReportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("invalid endorsement type: %s. Must be one of: all, trust-anchor, reference-value", endorsementType), + ) + return + } + + o.logger.Debugw("GetEndorsements", "key-prefix", keyPrefix, "type", endorsementType) + + resp, err := o.Provisioner.GetEndorsements(keyPrefix, endorsementType) + if err != nil { + o.logger.Errorw("get endorsements failed", "error", err) + ReportProblem(c, + http.StatusInternalServerError, + fmt.Sprintf("error retrieving endorsements: %v", err), + ) + return + } + + if resp.Status != nil && !resp.Status.Result { + ReportProblem(c, + http.StatusInternalServerError, + fmt.Sprintf("get endorsements failed: %s", resp.Status.ErrorDetail), + ) + return + } + + c.Header("Content-Type", "application/json") + c.JSON(http.StatusOK, resp) +} + +func (o *Handler) DeleteEndorsements(c *gin.Context) { + // Get query parameter or path parameter + key := c.Query("key") + if key == "" { + key = c.Param("key") + } + + if key == "" { + ReportProblem(c, + http.StatusBadRequest, + "key parameter is required", + ) + return + } + + endorsementType := c.Query("type") + + // Default to "all" if type is not specified + if endorsementType == "" { + endorsementType = "all" + } + + // Validate endorsement type + validTypes := map[string]bool{ + "all": true, + "trust-anchor": true, + "reference-value": true, + } + if !validTypes[endorsementType] { + ReportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("invalid endorsement type: %s. Must be one of: all, trust-anchor, reference-value", endorsementType), + ) + return + } + + o.logger.Debugw("DeleteEndorsements", "key", key, "type", endorsementType) + + resp, err := o.Provisioner.DeleteEndorsements(key, endorsementType) + if err != nil { + o.logger.Errorw("delete endorsements failed", "error", err) + ReportProblem(c, + http.StatusInternalServerError, + fmt.Sprintf("error deleting endorsements: %v", err), + ) + return + } + + if resp.Status != nil && !resp.Status.Result { + ReportProblem(c, + http.StatusInternalServerError, + fmt.Sprintf("delete endorsements failed: %s", resp.Status.ErrorDetail), + ) + return + } + + c.Header("Content-Type", "application/json") + c.JSON(http.StatusOK, resp) +} diff --git a/provisioning/api/handler_test.go b/provisioning/api/handler_test.go index bee17eb6..eb75f5e3 100644 --- a/provisioning/api/handler_test.go +++ b/provisioning/api/handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package api @@ -382,3 +382,175 @@ func TestHandler_GetWellKnownProvisioningInfo_UnsupportedAccept(t *testing.T) { assert.Equal(t, expectedType, w.Result().Header.Get("Content-Type")) assert.Equal(t, expectedBody, body) } + +func TestHandler_GetEndorsements_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectedEndorsements := []*proto.EndorsementEntry{ + { + Key: "test-key-1", + Values: []string{"value1", "value2"}, + }, + { + Key: "test-key-2", + Values: []string{"value3"}, + }, + } + + expectedResp := &proto.GetEndorsementsResponse{ + Endorsements: expectedEndorsements, + Status: &proto.Status{ + Result: true, + }, + } + + dm := mock_deps.NewMockIProvisioner(ctrl) + dm.EXPECT(). + GetEndorsements(gomock.Eq(""), gomock.Eq("all")). + Return(expectedResp, nil) + + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodGet, "/endorsement-provisioning/v1/endorsements", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Result().Header.Get("Content-Type")) + + var body proto.GetEndorsementsResponse + _ = json.Unmarshal(w.Body.Bytes(), &body) + + assert.Equal(t, len(expectedEndorsements), len(body.Endorsements)) +} + +func TestHandler_GetEndorsements_WithFilter(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectedResp := &proto.GetEndorsementsResponse{ + Endorsements: []*proto.EndorsementEntry{ + { + Key: "prefix-key-1", + Values: []string{"value1"}, + }, + }, + Status: &proto.Status{ + Result: true, + }, + } + + dm := mock_deps.NewMockIProvisioner(ctrl) + dm.EXPECT(). + GetEndorsements(gomock.Eq("prefix"), gomock.Eq("trust-anchor")). + Return(expectedResp, nil) + + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodGet, "/endorsement-provisioning/v1/endorsements?key-prefix=prefix&type=trust-anchor", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestHandler_GetEndorsements_InvalidType(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dm := mock_deps.NewMockIProvisioner(ctrl) + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodGet, "/endorsement-provisioning/v1/endorsements?type=invalid", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestHandler_DeleteEndorsements_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + expectedResp := &proto.DeleteEndorsementsResponse{ + DeletedCount: 2, + Status: &proto.Status{ + Result: true, + }, + } + + dm := mock_deps.NewMockIProvisioner(ctrl) + dm.EXPECT(). + DeleteEndorsements(gomock.Eq("test-key"), gomock.Eq("all")). + Return(expectedResp, nil) + + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodDelete, "/endorsement-provisioning/v1/endorsements?key=test-key", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusOK, w.Code) + + var body proto.DeleteEndorsementsResponse + _ = json.Unmarshal(w.Body.Bytes(), &body) + + assert.Equal(t, int32(2), body.DeletedCount) +} + +func TestHandler_DeleteEndorsements_MissingKey(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dm := mock_deps.NewMockIProvisioner(ctrl) + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodDelete, "/endorsement-provisioning/v1/endorsements", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusBadRequest, w.Code) +} + +func TestHandler_DeleteEndorsements_Error(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + dm := mock_deps.NewMockIProvisioner(ctrl) + dm.EXPECT(). + DeleteEndorsements(gomock.Eq("test-key"), gomock.Eq("all")). + Return(nil, errors.New("test error")) + + h := NewHandler(dm, log.Named("test")) + + w := httptest.NewRecorder() + g, _ := gin.CreateTestContext(w) + + g.Request, _ = http.NewRequest(http.MethodDelete, "/endorsement-provisioning/v1/endorsements?key=test-key", http.NoBody) + + u := auth.NewPassthroughAuthorizer(log.Named("auth")) + NewRouter(h, u).ServeHTTP(w, g.Request) + + assert.Equal(t, http.StatusInternalServerError, w.Code) +} diff --git a/provisioning/api/mocks/iprovisioner.go b/provisioning/api/mocks/iprovisioner.go index 8fa86c83..1aa25727 100644 --- a/provisioning/api/mocks/iprovisioner.go +++ b/provisioning/api/mocks/iprovisioner.go @@ -1,5 +1,7 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ../provisioner/iprovisioner.go +// Copyright 2022-2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 // Package mocks is a generated GoMock package. package mocks @@ -92,3 +94,33 @@ func (mr *MockIProvisionerMockRecorder) SupportedMediaTypes() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedMediaTypes", reflect.TypeOf((*MockIProvisioner)(nil).SupportedMediaTypes)) } + +// GetEndorsements mocks base method. +func (m *MockIProvisioner) GetEndorsements(keyPrefix string, endorsementType string) (*proto.GetEndorsementsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEndorsements", keyPrefix, endorsementType) + ret0, _ := ret[0].(*proto.GetEndorsementsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEndorsements indicates an expected call of GetEndorsements. +func (mr *MockIProvisionerMockRecorder) GetEndorsements(keyPrefix, endorsementType interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEndorsements", reflect.TypeOf((*MockIProvisioner)(nil).GetEndorsements), keyPrefix, endorsementType) +} + +// DeleteEndorsements mocks base method. +func (m *MockIProvisioner) DeleteEndorsements(key string, endorsementType string) (*proto.DeleteEndorsementsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteEndorsements", key, endorsementType) + ret0, _ := ret[0].(*proto.DeleteEndorsementsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteEndorsements indicates an expected call of DeleteEndorsements. +func (mr *MockIProvisionerMockRecorder) DeleteEndorsements(key, endorsementType interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEndorsements", reflect.TypeOf((*MockIProvisioner)(nil).DeleteEndorsements), key, endorsementType) +} diff --git a/provisioning/api/router.go b/provisioning/api/router.go index 24a665da..df2cd9a2 100644 --- a/provisioning/api/router.go +++ b/provisioning/api/router.go @@ -12,7 +12,7 @@ import ( var publicApiMap = make(map[string]string) const ( - provisioningPath = "/endorsement-provisioning/v1" + provisioningPath = "/endorsement-provisioning/v1" getWellKnownProvisioningInfoPath = "/.well-known/veraison/provisioning" ) @@ -30,6 +30,11 @@ func NewRouter(handler IHandler, authorizer auth.IAuthorizer) *gin.Engine { provGroup.POST("submit", handler.Submit) publicApiMap["provisioningSubmit"] = path.Join(provisioningPath, "submit") + provGroup.GET("endorsements", handler.GetEndorsements) + publicApiMap["provisioningGetEndorsements"] = path.Join(provisioningPath, "endorsements") + + provGroup.DELETE("endorsements", handler.DeleteEndorsements) + publicApiMap["provisioningDeleteEndorsements"] = path.Join(provisioningPath, "endorsements") return router } diff --git a/provisioning/provisioner/iprovisioner.go b/provisioning/provisioner/iprovisioner.go index 7c88c217..1b5db999 100644 --- a/provisioning/provisioner/iprovisioner.go +++ b/provisioning/provisioner/iprovisioner.go @@ -1,4 +1,4 @@ -// Copyright 2022-2023 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package provisioner @@ -9,4 +9,6 @@ type IProvisioner interface { IsSupportedMediaType(mt string) (bool, error) SupportedMediaTypes() ([]string, error) SubmitEndorsements(tenantID string, data []byte, mt string) error + GetEndorsements(keyPrefix string, endorsementType string) (*proto.GetEndorsementsResponse, error) + DeleteEndorsements(key string, endorsementType string) (*proto.DeleteEndorsementsResponse, error) } diff --git a/provisioning/provisioner/provisioner.go b/provisioning/provisioner/provisioner.go index c7778d44..f313d14e 100644 --- a/provisioning/provisioner/provisioner.go +++ b/provisioning/provisioner/provisioner.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package provisioner @@ -84,3 +84,19 @@ func (p *Provisioner) SubmitEndorsements(tenantID string, data []byte, mt string func (p *Provisioner) GetVTSState() (*proto.ServiceState, error) { return p.VTSClient.GetServiceState(context.TODO(), &emptypb.Empty{}) } + +func (p *Provisioner) GetEndorsements(keyPrefix string, endorsementType string) (*proto.GetEndorsementsResponse, error) { + req := &proto.GetEndorsementsRequest{ + KeyPrefix: keyPrefix, + EndorsementType: endorsementType, + } + return p.VTSClient.GetEndorsements(context.Background(), req) +} + +func (p *Provisioner) DeleteEndorsements(key string, endorsementType string) (*proto.DeleteEndorsementsResponse, error) { + req := &proto.DeleteEndorsementsRequest{ + Key: key, + EndorsementType: endorsementType, + } + return p.VTSClient.DeleteEndorsements(context.Background(), req) +} diff --git a/vts/trustedservices/trustedservices_grpc.go b/vts/trustedservices/trustedservices_grpc.go index fe876708..18be1034 100644 --- a/vts/trustedservices/trustedservices_grpc.go +++ b/vts/trustedservices/trustedservices_grpc.go @@ -119,7 +119,7 @@ func (o *GRPC) Init( cfg := GRPCConfig{ ServerAddress: DefaultVTSAddr, - UseTLS: true, + UseTLS: true, } loader := config.NewLoader(&cfg) @@ -294,6 +294,167 @@ func submitEndorsementErrorResponse(err error) *proto.SubmitEndorsementsResponse } } +func (o *GRPC) GetEndorsements(ctx context.Context, req *proto.GetEndorsementsRequest) (*proto.GetEndorsementsResponse, error) { + o.logger.Debugw("GetEndorsements", "key-prefix", req.KeyPrefix, "type", req.EndorsementType) + + var endorsements []*proto.EndorsementEntry + endorsementType := strings.ToLower(req.EndorsementType) + + // Default to "all" if not specified + if endorsementType == "" { + endorsementType = "all" + } + + // Get endorsements from trust anchor store + if endorsementType == "trust-anchor" || endorsementType == "all" { + taEndorsements, err := o.getEndorsementsFromStore(o.TaStore, req.KeyPrefix) + if err != nil { + return &proto.GetEndorsementsResponse{ + Status: &proto.Status{ + Result: false, + ErrorDetail: fmt.Sprintf("error retrieving trust anchors: %v", err), + }, + }, nil + } + endorsements = append(endorsements, taEndorsements...) + } + + // Get endorsements from reference value store + if endorsementType == "reference-value" || endorsementType == "all" { + enEndorsements, err := o.getEndorsementsFromStore(o.EnStore, req.KeyPrefix) + if err != nil { + return &proto.GetEndorsementsResponse{ + Status: &proto.Status{ + Result: false, + ErrorDetail: fmt.Sprintf("error retrieving reference values: %v", err), + }, + }, nil + } + endorsements = append(endorsements, enEndorsements...) + } + + return &proto.GetEndorsementsResponse{ + Endorsements: endorsements, + Status: &proto.Status{ + Result: true, + }, + }, nil +} + +func (o *GRPC) getEndorsementsFromStore(store kvstore.IKVStore, keyPrefix string) ([]*proto.EndorsementEntry, error) { + var entries []*proto.EndorsementEntry + + keys, err := store.GetKeys() + if err != nil { + return nil, err + } + + for _, key := range keys { + // Filter by prefix if specified + if keyPrefix != "" && !strings.HasPrefix(key, keyPrefix) { + continue + } + + values, err := store.Get(key) + if err != nil { + o.logger.Warnw("error getting values for key", "key", key, "error", err) + continue + } + + entries = append(entries, &proto.EndorsementEntry{ + Key: key, + Values: values, + }) + } + + return entries, nil +} + +func (o *GRPC) DeleteEndorsements(ctx context.Context, req *proto.DeleteEndorsementsRequest) (*proto.DeleteEndorsementsResponse, error) { + o.logger.Debugw("DeleteEndorsements", "key", req.Key, "type", req.EndorsementType) + + if req.Key == "" { + return &proto.DeleteEndorsementsResponse{ + DeletedCount: 0, + Status: &proto.Status{ + Result: false, + ErrorDetail: "key is required", + }, + }, nil + } + + endorsementType := strings.ToLower(req.EndorsementType) + + // Default to "all" if not specified + if endorsementType == "" { + endorsementType = "all" + } + + var totalDeleted int32 + + // Delete from trust anchor store + if endorsementType == "trust-anchor" || endorsementType == "all" { + deleted, err := o.deleteEndorsementsFromStore(o.TaStore, req.Key) + if err != nil { + return &proto.DeleteEndorsementsResponse{ + DeletedCount: 0, + Status: &proto.Status{ + Result: false, + ErrorDetail: fmt.Sprintf("error deleting trust anchors: %v", err), + }, + }, nil + } + totalDeleted += deleted + } + + // Delete from reference value store + if endorsementType == "reference-value" || endorsementType == "all" { + deleted, err := o.deleteEndorsementsFromStore(o.EnStore, req.Key) + if err != nil { + return &proto.DeleteEndorsementsResponse{ + DeletedCount: totalDeleted, + Status: &proto.Status{ + Result: false, + ErrorDetail: fmt.Sprintf("error deleting reference values: %v", err), + }, + }, nil + } + totalDeleted += deleted + } + + o.logger.Infow("deleted endorsements", "count", totalDeleted, "key", req.Key) + + return &proto.DeleteEndorsementsResponse{ + DeletedCount: totalDeleted, + Status: &proto.Status{ + Result: true, + }, + }, nil +} + +func (o *GRPC) deleteEndorsementsFromStore(store kvstore.IKVStore, keyPattern string) (int32, error) { + var deleted int32 + + keys, err := store.GetKeys() + if err != nil { + return 0, err + } + + for _, key := range keys { + // Match exact key or prefix + if key == keyPattern || strings.HasPrefix(key, keyPattern) { + err := store.Del(key) + if err != nil && !errors.Is(err, kvstore.ErrKeyNotFound) { + o.logger.Warnw("error deleting key", "key", key, "error", err) + continue + } + deleted++ + } + } + + return deleted, nil +} + func (o *GRPC) addRefValues(ctx context.Context, refVal *handler.Endorsement) error { var ( err error diff --git a/vtsclient/vtsclient_grpc.go b/vtsclient/vtsclient_grpc.go index 6d011182..e284f128 100644 --- a/vtsclient/vtsclient_grpc.go +++ b/vtsclient/vtsclient_grpc.go @@ -1,4 +1,4 @@ -// Copyright 2022-2024 Contributors to the Veraison project. +// Copyright 2022-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package vtsclient @@ -182,6 +182,36 @@ func (o *GRPC) EnsureConnection() error { return nil } +func (o *GRPC) GetEndorsements( + ctx context.Context, in *proto.GetEndorsementsRequest, opts ...grpc.CallOption, +) (*proto.GetEndorsementsResponse, error) { + if err := o.EnsureConnection(); err != nil { + return nil, NewNoConnectionError("GetEndorsements", err) + } + + c := o.GetProvisionerClient() + if c == nil { + return nil, ErrNoClient + } + + return c.GetEndorsements(ctx, in, opts...) +} + +func (o *GRPC) DeleteEndorsements( + ctx context.Context, in *proto.DeleteEndorsementsRequest, opts ...grpc.CallOption, +) (*proto.DeleteEndorsementsResponse, error) { + if err := o.EnsureConnection(); err != nil { + return nil, NewNoConnectionError("DeleteEndorsements", err) + } + + c := o.GetProvisionerClient() + if c == nil { + return nil, ErrNoClient + } + + return c.DeleteEndorsements(ctx, in, opts...) +} + func (o *GRPC) GetEARSigningPublicKey(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*proto.PublicKey, error) { if err := o.EnsureConnection(); err != nil { return nil, NewNoConnectionError("GetEARSigningPublicKey", err)