Compare commits

..

20 Commits

Author SHA1 Message Date
Drasko DRASKOVIC c641cbebf8 Update ELASTIC reference 2025-07-27 16:48:13 +02:00
Sammy Kerata Oina 4e8057f481 COCOS-460 - Restore test coverage to 65% (#465)
CI / ci (push) Has been cancelled
* Implement IMAMeasurements method in agentSDK and add corresponding unit tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add unit tests for NewIMAMeasurements command in CLI

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add error assertion for command execution in NewIMAMeasurements test

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix nil pointer dereference in Close method and update NewCreateVMCmd logic for manager client initialization

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactor file permission settings to use octal notation and improve cleanup handling in NewCreateVMCmd test

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add comprehensive unit tests for state machine functionality

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add mock implementation for Algorithm interface and corresponding test cases

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactor file permission settings to use octal notation in TestStopComputationIntegration

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove redundant reset test cases from TestStateMachine_Reset

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix race condition in action call verification in TestStateMachine_HandleEvent

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance state machine with reset functionality and improve thread safety in event handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Improve error handling in state machine start function during tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove concurrent reset and send event test from state machine tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove error logging for Start function in transition tests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add mock implementations for AgentService_IMAMeasurementsClient and Service Shutdown method; enhance progress tests for IMA measurements handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add comprehensive tests for FileStorage functionality including loading, saving, and concurrent access

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance tests by adding dataset and algorithm hashes in handleRunReqChunks; improve error handling in TestFileStorage_ErrorHandling cleanup

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance TestManagerClient_Process by adding new test cases for Agent state and Disconnect requests; update setupMocks to include grpcClient

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix graceful shutdown in gRPC server by adding nil checks for health and server instances

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance TestAttestation by adding mock expectations for VTpmAttestation and Attestation methods; update service call to include platform parameter

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance gRPC Server by adding synchronization for start/stop methods; prevent multiple starts and ensure graceful shutdown

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add unit tests for gRPC server methods including VM creation, removal, and info retrieval

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add tests for SEVSNP and TDX host capabilities; remove unused vsock code

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add a newline for better readability in vm_test.go

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add integration tests for gRPC client in cvm_test.go

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove unused vsock dependencies and add comprehensive unit tests for GCP attestation functions

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Skip GCP tests if credentials are not set

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add tests for error handling in attestation configuration and GCP commands

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Improve error handling in Azure VM test response writing

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Skip tests in GCP functions if credentials are not set

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add comprehensive unit tests for Azure attestation provider and verifier

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add unit tests for TPM functionality and improve error handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add comprehensive tests for attestation functionality and improve error handling

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add validation for teeNonce in TeeAttestation and implement comprehensive tests for provider methods

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactor error messages in TDX attestation tests for clarity

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix error message in TeeAttestation test for valid nonce case

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add MeasurementProvider mock and update mockery configuration

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add logging for product in parseUints and rename test functions for clarity

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Refactor TestSevsnpverify to reset configuration and improve error logging

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-07-25 15:35:37 +02:00
Sammy Kerata Oina 85a2b7a6c8 COCOS-430 - Refactor gRPC server handlers to use a map for improved organization and add validation for nonce lengths in attestation requests (#477)
* Refactor gRPC server handlers to use a map for improved organization and add validation for nonce lengths in attestation requests

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance nonce validation and improve error handling in gRPC server methods

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-07-14 13:46:02 +02:00
Sammy Kerata Oina 45187d7f41 COCOS-454 - Implement graceful shutdown for services and add TTL management for VMs (#473)
* Implement graceful shutdown for services and add TTL management for VMs

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Remove unnecessary comment from go-tdx-guest dependency in go.mod

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Update manager/api/logging.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add TTL manager initialization in TestStop

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix logging format in Shutdown method for consistency

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add unit tests for TTL manager functionality

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance TTL tests with mutex for thread safety in expiration checks

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add TTL parameter to CreateVM in TestRun for improved testing scenarios

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add Shutdown test to verify VM cleanup and TTL manager integration

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-11 16:21:29 +02:00
dorcaslitunya f543cb4363 COCOS-456 Remove SEV support from repo (#472)
* Remove SEV support from repo

* Remove SEV references
2025-07-10 10:58:36 +02:00
dependabot[bot] cef47baed7 Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc (#471)
Bumps [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.61.0 to 0.62.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.61.0...zpages/v0.62.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
  dependency-version: 0.62.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 19:32:36 +02:00
Danko Miladinovic 698bd948ed COCOS-474 - New aTLS implementation (#475)
* initial new aTLS

* add CA API call for aTLS
2025-07-08 14:54:57 +02:00
Danko Miladinovic 9c8ddfd2b1 COCOS-439 - TDX Attestation support (#459)
CI / ci (push) Has been cancelled
* inital tdx attestation support

* fix fetching and verification errors

* fix tests

* fix lint problems

* fix reading attestation policy

* add tdx package

* remove Makefile configuration for staticly built binaries
2025-07-01 00:44:29 +02:00
Sammy Kerata Oina 79c66a89c3 NOISSUE - Fix TDX qemu command (#468)
CI / ci (push) Has been cancelled
* Fix condition to check for both SEVSNP and TDX configurations before copying firmware vars file

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix TDXObject string formatting by removing unnecessary quotes

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-06-27 18:29:31 +02:00
Sammy Kerata Oina f52702b631 NOISSUE - Fix TDXEnabled function to check for 'Y' instead of '1' in kernel parameter (#467)
* Fix TDXEnabled function to check for 'Y' instead of '1' in kernel parameter

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix TDXEnabled test to check for 'Y' instead of '1' in CPU flags

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-06-27 11:43:07 +02:00
Jovan Djukic 31c7833c3d NOISSUE - Make data paths optional (#466)
CI / ci (push) Has been cancelled
2025-06-25 14:48:55 +02:00
Sammy Kerata Oina 64bf7a56ac NOISSUE - Fix SEVSNPEnabled function to check for 'Y' instead of '1' in kernel parameter (#461)
CI / ci (push) Has been cancelled
* Fix SEVSNPEnabled function to check for 'Y' instead of '1' in kernel parameter

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Fix TestSEVSNPEnabled to check for 'Y' instead of '1' in KVM parameter

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-06-23 16:33:17 +02:00
Danko Miladinovic bd59a4a617 NOISSUE - Fix attestation policy to JSON (#453)
CI / ci (push) Has been cancelled
2025-06-17 17:12:28 +02:00
dependabot[bot] c9af8a166b Bump cloud.google.com/go/storage from 1.51.0 to 1.55.0 (#448)
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.51.0 to 1.55.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/spanner/v1.51.0...spanner/v1.55.0)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/storage
  dependency-version: 1.55.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 14:50:41 +02:00
dependabot[bot] 17c6accbff Bump github.com/docker/docker (#447)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 28.0.4+incompatible to 28.2.2+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v28.0.4...v28.2.2)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-version: 28.2.2+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 14:49:59 +02:00
dependabot[bot] 2d6d276061 Bump go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc (#450)
Bumps [go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc](https://github.com/open-telemetry/opentelemetry-go-contrib) from 0.60.0 to 0.61.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.60.0...zpages/v0.61.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
  dependency-version: 0.61.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 14:49:33 +02:00
dependabot[bot] e8c2ccc071 Bump github.com/google/go-tpm from 0.9.3 to 0.9.5 (#451)
Bumps [github.com/google/go-tpm](https://github.com/google/go-tpm) from 0.9.3 to 0.9.5.
- [Release notes](https://github.com/google/go-tpm/releases)
- [Commits](https://github.com/google/go-tpm/compare/v0.9.3...v0.9.5)

---
updated-dependencies:
- dependency-name: github.com/google/go-tpm
  dependency-version: 0.9.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 14:47:54 +02:00
dependabot[bot] f1af397aa0 Bump google.golang.org/grpc from 1.72.1 to 1.73.0 (#455)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.72.1 to 1.73.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.72.1...v1.73.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.73.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 14:47:45 +02:00
dorcaslitunya 77325753f8 NOISSUE - Add TDX support to Manager (#446)
CI / ci (push) Has been cancelled
* Add TDX support on manager

* Add functions to check platform

* Search for tdx in kernel parameters

* Modify based on comments
2025-06-17 11:34:30 +02:00
Danko Miladinovic 3e474338c5 NOISSUE - Extend the PCR16 with computation manifest JSON hash (#457)
CI / ci (push) Has been cancelled
* extend PCR16 with manifest

* fix ci

* fix PCR16 extension

* move pcr extension to executeRun

* put extension in a defer block
2025-06-12 18:49:14 +02:00
92 changed files with 12771 additions and 3597 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
BUILD_DIR = build
SERVICES = manager agent cli
ATTESTATION_POLICY = attestation_policy
CGO_ENABLED ?= 1
CGO_ENABLED ?= 0
GOARCH ?= amd64
VERSION ?= $(shell git describe --abbrev=0 --tags --always)
COMMIT ?= $(shell git rev-parse HEAD)
+1 -1
View File
@@ -77,4 +77,4 @@ Cocos AI is published under the permissive open-source [Apache-2.0](LICENSE) lic
- [Confidential Computing Overview](https://confidentialcomputing.io/white-papers-reports/)
- [Trusted Execution Environments (TEEs)](https://en.wikipedia.org/wiki/Trusted_execution_environment)
>This work has been partially supported by the ELASTIC project, which received funding from the Smart Networks and Services Joint Undertaking (SNS JU) under the European Unions Horizon Europe research and innovation programme under Grant Agreement No 101139067. Views and opinions expressed are however those of the author(s) only and do not necessarily reflect those of the European Union. Neither the European Union nor the granting authority can be held responsible for them.
>This work has been partially supported by the [ELASTIC project](https://elasticproject.eu/), which received funding from the Smart Networks and Services Joint Undertaking (SNS JU) under the European Unions Horizon Europe research and innovation programme under [Grant Agreement No. 101139067](https://cordis.europa.eu/project/id/101139067). Views and opinions expressed are however those of the author(s) only and do not necessarily reflect those of the European Union. Neither the European Union nor the granting authority can be held responsible for them.
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery v2.53.3. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// Algorithm is an autogenerated mock type for the Algorithm type
type Algorithm struct {
mock.Mock
}
type Algorithm_Expecter struct {
mock *mock.Mock
}
func (_m *Algorithm) EXPECT() *Algorithm_Expecter {
return &Algorithm_Expecter{mock: &_m.Mock}
}
// Run provides a mock function with no fields
func (_m *Algorithm) Run() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Run")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Algorithm_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type Algorithm_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
func (_e *Algorithm_Expecter) Run() *Algorithm_Run_Call {
return &Algorithm_Run_Call{Call: _e.mock.On("Run")}
}
func (_c *Algorithm_Run_Call) Run(run func()) *Algorithm_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Algorithm_Run_Call) Return(_a0 error) *Algorithm_Run_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Algorithm_Run_Call) RunAndReturn(run func() error) *Algorithm_Run_Call {
_c.Call.Return(run)
return _c
}
// Stop provides a mock function with no fields
func (_m *Algorithm) Stop() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Stop")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Algorithm_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
type Algorithm_Stop_Call struct {
*mock.Call
}
// Stop is a helper method to define mock.On call
func (_e *Algorithm_Expecter) Stop() *Algorithm_Stop_Call {
return &Algorithm_Stop_Call{Call: _e.mock.On("Stop")}
}
func (_c *Algorithm_Stop_Call) Run(run func()) *Algorithm_Stop_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Algorithm_Stop_Call) Return(_a0 error) *Algorithm_Stop_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Algorithm_Stop_Call) RunAndReturn(run func() error) *Algorithm_Stop_Call {
_c.Call.Return(run)
return _c
}
// NewAlgorithm creates a new instance of Algorithm. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAlgorithm(t interface {
mock.TestingT
Cleanup(func())
}) *Algorithm {
mock := &Algorithm{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+1 -1
View File
@@ -62,7 +62,7 @@ func (req FetchAttestationResultReq) validate() error {
func validateAttestationType(attType attestation.PlatformType) error {
switch attType {
case attestation.SNP, attestation.VTPM, attestation.SNPvTPM, attestation.AzureToken:
case attestation.SNP, attestation.VTPM, attestation.SNPvTPM, attestation.TDX, attestation.AzureToken:
return nil
default:
return errors.New("invalid attestation type")
+290 -185
View File
@@ -10,6 +10,7 @@ import (
"io"
"strconv"
"github.com/go-kit/kit/endpoint"
"github.com/go-kit/kit/transport/grpc"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/pkg/attestation"
@@ -34,54 +35,69 @@ var (
var _ agent.AgentServiceServer = (*grpcServer)(nil)
type grpcServer struct {
algo grpc.Handler
data grpc.Handler
result grpc.Handler
attestation grpc.Handler
imaMeasurements grpc.Handler
attestationResult grpc.Handler
handlers map[string]grpc.Handler
agent.UnimplementedAgentServiceServer
}
type endpointConfig struct {
endpoint func(agent.Service) endpoint.Endpoint
decodeRequest grpc.DecodeRequestFunc
encodeResponse grpc.EncodeResponseFunc
}
// NewServer returns new AgentServiceServer instance.
func NewServer(svc agent.Service) agent.AgentServiceServer {
// Define endpoint configurations
endpoints := map[string]endpointConfig{
"algo": {
endpoint: algoEndpoint,
decodeRequest: decodeAlgoRequest,
encodeResponse: encodeAlgoResponse,
},
"data": {
endpoint: dataEndpoint,
decodeRequest: decodeDataRequest,
encodeResponse: encodeDataResponse,
},
"result": {
endpoint: resultEndpoint,
decodeRequest: decodeResultRequest,
encodeResponse: encodeResultResponse,
},
"attestation": {
endpoint: attestationEndpoint,
decodeRequest: decodeAttestationRequest,
encodeResponse: encodeAttestationResponse,
},
"imaMeasurements": {
endpoint: imaMeasurementsEndpoint,
decodeRequest: decodeIMAMeasurementsRequest,
encodeResponse: encodeIMAMeasurementsResponse,
},
"attestationResult": {
endpoint: attestationResultEndpoint,
decodeRequest: decodeAttestationResultRequest,
encodeResponse: encodeAttestationResultResponse,
},
}
// Create handlers using the configurations
handlers := make(map[string]grpc.Handler)
for name, config := range endpoints {
handlers[name] = grpc.NewServer(
config.endpoint(svc),
config.decodeRequest,
config.encodeResponse,
)
}
return &grpcServer{
algo: grpc.NewServer(
algoEndpoint(svc),
decodeAlgoRequest,
encodeAlgoResponse,
),
data: grpc.NewServer(
dataEndpoint(svc),
decodeDataRequest,
encodeDataResponse,
),
result: grpc.NewServer(
resultEndpoint(svc),
decodeResultRequest,
encodeResultResponse,
),
attestation: grpc.NewServer(
attestationEndpoint(svc),
decodeAttestationRequest,
encodeAttestationResponse,
),
imaMeasurements: grpc.NewServer(
imaMeasurementsEndpoint(svc),
decodeIMAMeasurementsRequest,
encodeIMAMeasurementsResponse,
),
attestationResult: grpc.NewServer(
attestationResultEndpoint(svc),
decodeAttestationResultRequest,
encodeAttestationResultResponse,
),
handlers: handlers,
}
}
func decodeAlgoRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*agent.AlgoRequest)
return algoReq{
Algorithm: req.Algorithm,
Requirements: req.Requirements,
@@ -94,7 +110,6 @@ func encodeAlgoResponse(_ context.Context, response interface{}) (interface{}, e
func decodeDataRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*agent.DataRequest)
return dataReq{
Dataset: req.Dataset,
Filename: req.Filename,
@@ -116,22 +131,47 @@ func encodeResultResponse(_ context.Context, response interface{}) (interface{},
}, nil
}
func validateNonce(nonce []byte, maxLen int, target interface{}) error {
if len(nonce) > maxLen {
switch maxLen {
case quoteprovider.Nonce:
return ErrTEENonceLength
case vtpm.Nonce:
return ErrVTPMNonceLength
default:
return ErrTokenNonceLength
}
}
switch t := target.(type) {
case *[quoteprovider.Nonce]byte:
copy(t[:], nonce)
case *[vtpm.Nonce]byte:
copy(t[:], nonce)
default:
return fmt.Errorf("unsupported target type for nonce validation: %T", target)
}
return nil
}
func decodeAttestationRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*agent.AttestationRequest)
var reportData [quoteprovider.Nonce]byte
var nonce [vtpm.Nonce]byte
if len(req.TeeNonce) > quoteprovider.Nonce {
return nil, ErrTEENonceLength
if err := validateNonce(req.TeeNonce, quoteprovider.Nonce, &reportData); err != nil {
return nil, err
}
if len(req.VtpmNonce) > vtpm.Nonce {
return nil, ErrVTPMNonceLength
if err := validateNonce(req.VtpmNonce, vtpm.Nonce, &nonce); err != nil {
return nil, err
}
copy(reportData[:], req.TeeNonce)
copy(nonce[:], req.VtpmNonce)
return attestationReq{TeeNonce: reportData, VtpmNonce: nonce, AttType: attestation.PlatformType(req.Type)}, nil
return attestationReq{
TeeNonce: reportData,
VtpmNonce: nonce,
AttType: attestation.PlatformType(req.Type),
}, nil
}
func encodeAttestationResponse(_ context.Context, response interface{}) (interface{}, error) {
@@ -141,6 +181,20 @@ func encodeAttestationResponse(_ context.Context, response interface{}) (interfa
}, nil
}
func decodeAttestationResultRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*agent.AttestationResultRequest)
var nonce [vtpm.Nonce]byte
if err := validateNonce(req.TokenNonce, vtpm.Nonce, &nonce); err != nil {
return nil, err
}
return FetchAttestationResultReq{
tokenNonce: nonce,
AttType: attestation.PlatformType(req.Type),
}, nil
}
func encodeAttestationResultResponse(_ context.Context, response interface{}) (interface{}, error) {
res := response.(fetchAttestationResultRes)
return &agent.AttestationResultResponse{
@@ -148,127 +202,6 @@ func encodeAttestationResultResponse(_ context.Context, response interface{}) (i
}, nil
}
func decodeAttestationResultRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
req := grpcReq.(*agent.AttestationResultRequest)
var nonce [vtpm.Nonce]byte
if len(req.TokenNonce) > vtpm.Nonce {
return nil, ErrVTPMNonceLength
}
copy(nonce[:], req.TokenNonce)
return FetchAttestationResultReq{tokenNonce: nonce, AttType: attestation.PlatformType(req.Type)}, nil
}
// Algo implements agent.AgentServiceServer.
func (s *grpcServer) Algo(stream agent.AgentService_AlgoServer) error {
var algoFile, reqFile []byte
for {
algoChunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return status.Error(codes.Internal, err.Error())
}
algoFile = append(algoFile, algoChunk.Algorithm...)
reqFile = append(reqFile, algoChunk.Requirements...)
}
_, res, err := s.algo.ServeGRPC(stream.Context(), &agent.AlgoRequest{Algorithm: algoFile, Requirements: reqFile})
if err != nil {
return err
}
ar := res.(*agent.AlgoResponse)
return stream.SendAndClose(ar)
}
// Data implements agent.AgentServiceServer.
func (s *grpcServer) Data(stream agent.AgentService_DataServer) error {
var dataFile []byte
var filename string
for {
dataChunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return status.Error(codes.Internal, err.Error())
}
dataFile = append(dataFile, dataChunk.Dataset...)
filename = dataChunk.Filename
}
_, res, err := s.data.ServeGRPC(stream.Context(), &agent.DataRequest{Dataset: dataFile, Filename: filename})
if err != nil {
return err
}
ar := res.(*agent.DataResponse)
return stream.SendAndClose(ar)
}
func (s *grpcServer) Result(req *agent.ResultRequest, stream agent.AgentService_ResultServer) error {
_, res, err := s.result.ServeGRPC(stream.Context(), req)
if err != nil {
return err
}
rr := res.(*agent.ResultResponse)
if err := stream.SetHeader(metadata.New(map[string]string{FileSizeKey: fmt.Sprint(len(rr.File))})); err != nil {
return status.Error(codes.Internal, err.Error())
}
resultBuffer := bytes.NewBuffer(rr.File)
buf := make([]byte, bufferSize)
for {
n, err := resultBuffer.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return status.Error(codes.Internal, err.Error())
}
if err := stream.Send(&agent.ResultResponse{File: buf[:n]}); err != nil {
return status.Error(codes.Internal, err.Error())
}
}
return nil
}
func (s *grpcServer) Attestation(req *agent.AttestationRequest, stream agent.AgentService_AttestationServer) error {
_, res, err := s.attestation.ServeGRPC(stream.Context(), req)
if err != nil {
return err
}
rr := res.(*agent.AttestationResponse)
if err := stream.SetHeader(metadata.New(map[string]string{FileSizeKey: fmt.Sprint(len(rr.File))})); err != nil {
return status.Error(codes.Internal, err.Error())
}
attestationBuffer := bytes.NewBuffer(rr.File)
buf := make([]byte, bufferSize)
for {
n, err := attestationBuffer.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return status.Error(codes.Internal, err.Error())
}
if err := stream.Send(&agent.AttestationResponse{File: buf[:n]}); err != nil {
return status.Error(codes.Internal, err.Error())
}
}
return nil
}
func decodeIMAMeasurementsRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
return imaMeasurementsReq{}, nil
}
@@ -281,56 +214,228 @@ func encodeIMAMeasurementsResponse(_ context.Context, response interface{}) (int
}, nil
}
func (s *grpcServer) streamingHandler(
ctx context.Context,
handlerName string,
req interface{},
stream interface{},
sendFn func([]byte) error,
getFileData func(interface{}) []byte,
) error {
handler, ok := s.handlers[handlerName]
if !ok {
return status.Errorf(codes.NotFound, "handler %q not found", handlerName)
}
_, res, err := handler.ServeGRPC(ctx, req)
if err != nil {
return err
}
fileData := getFileData(res)
// Set file size header
if setter, ok := stream.(interface{ SetHeader(metadata.MD) error }); ok {
if err := setter.SetHeader(metadata.New(map[string]string{
FileSizeKey: fmt.Sprint(len(fileData)),
})); err != nil {
return status.Error(codes.Internal, err.Error())
}
}
// Stream the file data
return s.streamFileData(bytes.NewBuffer(fileData), sendFn)
}
func (s *grpcServer) streamFileData(buffer *bytes.Buffer, sendFn func([]byte) error) error {
buf := make([]byte, bufferSize)
for {
n, err := buffer.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return status.Error(codes.Internal, err.Error())
}
if err := sendFn(buf[:n]); err != nil {
return status.Error(codes.Internal, err.Error())
}
}
return nil
}
func receiveStreamingData(getData func() ([]byte, string, error)) ([]byte, string, error) {
var data []byte
var filename string
for {
chunk, fname, err := getData()
if err == io.EOF {
break
}
if err != nil {
return nil, "", status.Error(codes.Internal, err.Error())
}
data = append(data, chunk...)
if fname != "" {
filename = fname
}
}
return data, filename, nil
}
// Algo implements agent.AgentServiceServer.
func (s *grpcServer) Algo(stream agent.AgentService_AlgoServer) error {
algoFile, reqFile, err := s.receiveAlgoData(stream)
if err != nil {
return err
}
_, res, err := s.handlers["algo"].ServeGRPC(stream.Context(), &agent.AlgoRequest{
Algorithm: algoFile,
Requirements: reqFile,
})
if err != nil {
return err
}
return stream.SendAndClose(res.(*agent.AlgoResponse))
}
func (s *grpcServer) receiveAlgoData(stream agent.AgentService_AlgoServer) ([]byte, []byte, error) {
var algoFile, reqFile []byte
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, nil, status.Error(codes.Internal, err.Error())
}
algoFile = append(algoFile, chunk.Algorithm...)
reqFile = append(reqFile, chunk.Requirements...)
}
return algoFile, reqFile, nil
}
// Data implements agent.AgentServiceServer.
func (s *grpcServer) Data(stream agent.AgentService_DataServer) error {
dataFile, filename, err := receiveStreamingData(func() ([]byte, string, error) {
chunk, err := stream.Recv()
if err != nil {
return nil, "", err
}
return chunk.Dataset, chunk.Filename, nil
})
if err != nil {
return err
}
_, res, err := s.handlers["data"].ServeGRPC(stream.Context(), &agent.DataRequest{
Dataset: dataFile,
Filename: filename,
})
if err != nil {
return err
}
return stream.SendAndClose(res.(*agent.DataResponse))
}
func (s *grpcServer) Result(req *agent.ResultRequest, stream agent.AgentService_ResultServer) error {
return s.streamingHandler(
stream.Context(),
"result",
req,
stream,
func(data []byte) error {
return stream.Send(&agent.ResultResponse{File: data})
},
func(res interface{}) []byte {
return res.(*agent.ResultResponse).File
},
)
}
func (s *grpcServer) Attestation(req *agent.AttestationRequest, stream agent.AgentService_AttestationServer) error {
return s.streamingHandler(
stream.Context(),
"attestation",
req,
stream,
func(data []byte) error {
return stream.Send(&agent.AttestationResponse{File: data})
},
func(res interface{}) []byte {
return res.(*agent.AttestationResponse).File
},
)
}
func (s *grpcServer) IMAMeasurements(req *agent.IMAMeasurementsRequest, stream agent.AgentService_IMAMeasurementsServer) error {
_, res, err := s.imaMeasurements.ServeGRPC(stream.Context(), req)
_, res, err := s.handlers["imaMeasurements"].ServeGRPC(stream.Context(), req)
if err != nil {
return err
}
rr := res.(*agent.IMAMeasurementsResponse)
if err := stream.SetHeader(metadata.New(map[string]string{FileSizeKey: strconv.Itoa(len(rr.File))})); err != nil {
if err := stream.SetHeader(metadata.New(map[string]string{
FileSizeKey: strconv.Itoa(len(rr.File)),
})); err != nil {
return status.Error(codes.Internal, err.Error())
}
imaBuff := bytes.NewBuffer(rr.File)
pcr10Buff := bytes.NewBuffer(rr.Pcr10)
return s.streamDualBuffers(
bytes.NewBuffer(rr.File),
bytes.NewBuffer(rr.Pcr10),
func(fileData, pcr10Data []byte) error {
return stream.Send(&agent.IMAMeasurementsResponse{
File: fileData,
Pcr10: pcr10Data,
})
},
)
}
imaResBuff := make([]byte, bufferSize)
pcr10ResBuff := make([]byte, bufferSize)
func (s *grpcServer) streamDualBuffers(
buf1, buf2 *bytes.Buffer,
sendFn func([]byte, []byte) error,
) error {
buff1 := make([]byte, bufferSize)
buff2 := make([]byte, bufferSize)
for {
nIma, errIma := imaBuff.Read(imaResBuff)
if errIma != nil && errIma != io.EOF {
return status.Error(codes.Internal, errIma.Error())
n1, err1 := buf1.Read(buff1)
if err1 != nil && err1 != io.EOF {
return status.Error(codes.Internal, err1.Error())
}
nPcr, errPcr := pcr10Buff.Read(pcr10ResBuff)
if errPcr != nil && errPcr != io.EOF {
return status.Error(codes.Internal, errPcr.Error())
n2, err2 := buf2.Read(buff2)
if err2 != nil && err2 != io.EOF {
return status.Error(codes.Internal, err2.Error())
}
if nIma == 0 && errIma == io.EOF &&
nPcr == 0 && errPcr == io.EOF {
if n1 == 0 && err1 == io.EOF && n2 == 0 && err2 == io.EOF {
break
}
if err := stream.Send(&agent.IMAMeasurementsResponse{File: imaResBuff[:nIma], Pcr10: pcr10ResBuff[:nPcr]}); err != nil {
if err := sendFn(buff1[:n1], buff2[:n2]); err != nil {
return status.Error(codes.Internal, err.Error())
}
}
return nil
}
func (s *grpcServer) AttestationResult(ctx context.Context, req *agent.AttestationResultRequest) (*agent.AttestationResultResponse, error) {
_, res, err := s.attestationResult.ServeGRPC(ctx, req)
_, res, err := s.handlers["attestationResult"].ServeGRPC(ctx, req)
if err != nil {
return nil, err
}
rr, ok := res.(*agent.AttestationResultResponse)
rr, ok := res.(*agent.AttestationResultResponse)
if !ok {
return nil, status.Error(codes.Internal, "failed to cast response to FetchAttestationResultResponse")
return nil, status.Error(codes.Internal, "failed to cast response to AttestationResultResponse")
}
return rr, nil
+280 -24
View File
@@ -68,8 +68,9 @@ func (m *MockAgentService_ResultServer) Context() context.Context {
return m.ctx
}
func (m *MockAgentService_ResultServer) SetHeader(metadata.MD) error {
return nil
func (m *MockAgentService_ResultServer) SetHeader(md metadata.MD) error {
args := m.Called(md)
return args.Error(0)
}
func (m *MockAgentService_ResultServer) Send(resp *agent.ResultResponse) error {
@@ -92,8 +93,46 @@ func (m *MockAgentService_AttestationServer) Send(resp *agent.AttestationRespons
return args.Error(0)
}
func (m *MockAgentService_AttestationServer) SetHeader(metadata.MD) error {
return nil
func (m *MockAgentService_AttestationServer) SetHeader(md metadata.MD) error {
args := m.Called(md)
return args.Error(0)
}
type MockAgentService_IMAMeasurementsServer struct {
grpc.ServerStream
mock.Mock
ctx context.Context
}
func (m *MockAgentService_IMAMeasurementsServer) Context() context.Context {
return m.ctx
}
func (m *MockAgentService_IMAMeasurementsServer) Send(resp *agent.IMAMeasurementsResponse) error {
args := m.Called(resp)
return args.Error(0)
}
func (m *MockAgentService_IMAMeasurementsServer) SetHeader(md metadata.MD) error {
args := m.Called(md)
return args.Error(0)
}
func TestNewServer(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
grpcServer, ok := server.(*grpcServer)
assert.True(t, ok)
assert.NotNil(t, grpcServer.handlers)
assert.Len(t, grpcServer.handlers, 6) // Should have 6 handlers
// Check that all expected handlers are present
expectedHandlers := []string{"algo", "data", "result", "attestation", "imaMeasurements", "attestationResult"}
for _, handler := range expectedHandlers {
assert.Contains(t, grpcServer.handlers, handler)
assert.NotNil(t, grpcServer.handlers[handler])
}
}
func TestAlgo(t *testing.T) {
@@ -102,8 +141,8 @@ func TestAlgo(t *testing.T) {
mockStream := &MockAgentService_AlgoServer{ctx: context.Background()}
mockStream.On("Recv").Return(&agent.AlgoRequest{Algorithm: []byte("algo"), Requirements: []byte("req")}, nil).Once()
mockStream.On("Recv").Return(&agent.AlgoRequest{}, io.EOF)
mockStream.On("SendAndClose", &agent.AlgoResponse{}).Return(nil)
mockStream.On("Recv").Return(&agent.AlgoRequest{}, io.EOF).Once()
mockStream.On("SendAndClose", &agent.AlgoResponse{}).Return(nil).Once()
mockService.On("Algo", context.Background(), agent.Algorithm{Algorithm: []byte("algo"), Requirements: []byte("req")}).Return(nil)
@@ -114,14 +153,33 @@ func TestAlgo(t *testing.T) {
mockService.AssertExpectations(t)
}
func TestAlgoWithMultipleChunks(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
mockStream := &MockAgentService_AlgoServer{ctx: context.Background()}
mockStream.On("Recv").Return(&agent.AlgoRequest{Algorithm: []byte("algo"), Requirements: []byte("req")}, nil).Once()
mockStream.On("Recv").Return(&agent.AlgoRequest{Algorithm: []byte("2"), Requirements: []byte("2")}, nil).Once()
mockStream.On("Recv").Return(&agent.AlgoRequest{}, io.EOF).Once()
mockStream.On("SendAndClose", &agent.AlgoResponse{}).Return(nil).Once()
mockService.On("Algo", context.Background(), agent.Algorithm{Algorithm: []byte("algo2"), Requirements: []byte("req2")}).Return(nil)
err := server.Algo(mockStream)
assert.NoError(t, err)
mockStream.AssertExpectations(t)
mockService.AssertExpectations(t)
}
func TestData(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
mockStream := &MockAgentService_DataServer{ctx: context.Background()}
mockStream.On("Recv").Return(&agent.DataRequest{Dataset: []byte("data"), Filename: "test.txt"}, nil).Once()
mockStream.On("Recv").Return(&agent.DataRequest{}, io.EOF)
mockStream.On("SendAndClose", &agent.DataResponse{}).Return(nil)
mockStream.On("Recv").Return(&agent.DataRequest{}, io.EOF).Once()
mockStream.On("SendAndClose", &agent.DataResponse{}).Return(nil).Once()
mockService.On("Data", context.Background(), agent.Dataset{Dataset: []byte("data"), Filename: "test.txt"}).Return(nil)
@@ -136,9 +194,18 @@ func TestResult(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
resultData := []byte("result data")
mockStream := &MockAgentService_ResultServer{ctx: context.Background()}
mockService.On("Result", mock.Anything).Return([]byte("result data"), nil)
mockStream.On("Send", mock.AnythingOfType("*agent.ResultResponse")).Return(nil)
// Mock the SetHeader call
mockStream.On("SetHeader", mock.AnythingOfType("metadata.MD")).Return(nil).Once()
// Mock the Send call - it should be called with the result data
mockStream.On("Send", mock.MatchedBy(func(resp *agent.ResultResponse) bool {
return len(resp.File) > 0
})).Return(nil).Once()
mockService.On("Result", mock.Anything).Return(resultData, nil)
err := server.Result(&agent.ResultRequest{}, mockStream)
assert.NoError(t, err)
@@ -151,35 +218,137 @@ func TestAttestation(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
attestationData := []byte("attestation data")
mockStream := &MockAgentService_AttestationServer{ctx: context.Background()}
mockStream.On("Send", mock.AnythingOfType("*agent.AttestationResponse")).Return(nil)
// Mock the SetHeader call
mockStream.On("SetHeader", mock.AnythingOfType("metadata.MD")).Return(nil).Once()
// Mock the Send call
mockStream.On("Send", mock.MatchedBy(func(resp *agent.AttestationResponse) bool {
return len(resp.File) > 0
})).Return(nil).Once()
reportData := [quoteprovider.Nonce]byte{}
vtpmNonce := [vtpm.Nonce]byte{}
attestationType := attestation.SNP
mockService.On("Attestation", mock.Anything, reportData, vtpmNonce, attestationType).Return([]byte("attestation data"), nil)
mockService.On("Attestation", mock.Anything, reportData, vtpmNonce, attestationType).Return(attestationData, nil)
err := server.Attestation(&agent.AttestationRequest{TeeNonce: reportData[:]}, mockStream)
err := server.Attestation(&agent.AttestationRequest{TeeNonce: reportData[:], Type: int32(attestationType)}, mockStream)
assert.NoError(t, err)
mockService.AssertExpectations(t)
mockStream.AssertExpectations(t)
}
func TestIMAMeasurements(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
imaData := []byte("ima data")
pcr10Data := []byte("pcr10 data")
mockStream := &MockAgentService_IMAMeasurementsServer{ctx: context.Background()}
// Mock the SetHeader call
mockStream.On("SetHeader", mock.AnythingOfType("metadata.MD")).Return(nil).Once()
// Mock the Send call
mockStream.On("Send", mock.MatchedBy(func(resp *agent.IMAMeasurementsResponse) bool {
return len(resp.File) > 0 || len(resp.Pcr10) > 0
})).Return(nil).Once()
mockService.On("IMAMeasurements", mock.Anything).Return(imaData, pcr10Data, nil)
err := server.IMAMeasurements(&agent.IMAMeasurementsRequest{}, mockStream)
assert.NoError(t, err)
mockService.AssertExpectations(t)
mockStream.AssertExpectations(t)
}
func TestAttestationResult(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
attestationData := []byte("attestation result data")
vtpmNonce := [vtpm.Nonce]byte{}
attestationType := attestation.SNP
mockService.On("AttestationResult", mock.Anything, vtpmNonce, attestationType).Return([]byte("attestation data"), nil)
resp, err := server.AttestationResult(context.Background(), &agent.AttestationResultRequest{TokenNonce: vtpmNonce[:]})
mockService.On("AttestationResult", mock.Anything, vtpmNonce, attestationType).Return(attestationData, nil)
resp, err := server.AttestationResult(context.Background(), &agent.AttestationResultRequest{
TokenNonce: vtpmNonce[:],
Type: int32(attestationType),
})
assert.NoError(t, err)
assert.Equal(t, []byte("attestation data"), resp.File)
assert.Equal(t, attestationData, resp.File)
mockService.AssertExpectations(t)
}
func TestValidateNonce(t *testing.T) {
tests := []struct {
name string
nonce []byte
maxLen int
shouldError bool
expectedErr error
}{
{
name: "valid TEE nonce",
nonce: make([]byte, quoteprovider.Nonce),
maxLen: quoteprovider.Nonce,
shouldError: false,
},
{
name: "valid vTPM nonce",
nonce: make([]byte, vtpm.Nonce),
maxLen: vtpm.Nonce,
shouldError: false,
},
{
name: "TEE nonce too long",
nonce: make([]byte, quoteprovider.Nonce+1),
maxLen: quoteprovider.Nonce,
shouldError: true,
expectedErr: ErrTEENonceLength,
},
{
name: "vTPM nonce too long",
nonce: make([]byte, vtpm.Nonce+1),
maxLen: vtpm.Nonce,
shouldError: true,
expectedErr: ErrVTPMNonceLength,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.maxLen == quoteprovider.Nonce {
var target [quoteprovider.Nonce]byte
err := validateNonce(tt.nonce, tt.maxLen, &target)
if tt.shouldError {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr, err)
} else {
assert.NoError(t, err)
}
} else {
var target [vtpm.Nonce]byte
err := validateNonce(tt.nonce, tt.maxLen, &target)
if tt.shouldError {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr, err)
} else {
assert.NoError(t, err)
}
}
})
}
}
func TestDecodeAlgoRequest(t *testing.T) {
req := &agent.AlgoRequest{Algorithm: []byte("algo"), Requirements: []byte("req")}
decoded, err := decodeAlgoRequest(context.Background(), req)
@@ -219,11 +388,38 @@ func TestEncodeResultResponse(t *testing.T) {
}
func TestDecodeAttestationRequest(t *testing.T) {
nonce := [quoteprovider.Nonce]byte{}
req := &agent.AttestationRequest{TeeNonce: nonce[:]}
teeNonce := make([]byte, quoteprovider.Nonce)
vtpmNonce := make([]byte, vtpm.Nonce)
req := &agent.AttestationRequest{
TeeNonce: teeNonce,
VtpmNonce: vtpmNonce,
Type: int32(attestation.SNP),
}
decoded, err := decodeAttestationRequest(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, attestationReq{TeeNonce: nonce}, decoded)
decodedReq := decoded.(attestationReq)
assert.Equal(t, attestation.SNP, decodedReq.AttType)
}
func TestDecodeAttestationRequestWithInvalidNonce(t *testing.T) {
// Test with TEE nonce too long
teeNonce := make([]byte, quoteprovider.Nonce+1)
req := &agent.AttestationRequest{TeeNonce: teeNonce}
_, err := decodeAttestationRequest(context.Background(), req)
assert.Error(t, err)
assert.Equal(t, ErrTEENonceLength, err)
// Test with vTPM nonce too long
vtpmNonce := make([]byte, vtpm.Nonce+1)
req = &agent.AttestationRequest{VtpmNonce: vtpmNonce}
_, err = decodeAttestationRequest(context.Background(), req)
assert.Error(t, err)
assert.Equal(t, ErrVTPMNonceLength, err)
}
func TestEncodeAttestationResponse(t *testing.T) {
@@ -232,16 +428,76 @@ func TestEncodeAttestationResponse(t *testing.T) {
assert.Equal(t, &agent.AttestationResponse{File: []byte("attestation")}, encoded)
}
func TestDecodeAttestationResultRequest(t *testing.T) {
tokenNonce := make([]byte, vtpm.Nonce)
req := &agent.AttestationResultRequest{
TokenNonce: tokenNonce,
Type: int32(attestation.SNP),
}
decoded, err := decodeAttestationResultRequest(context.Background(), req)
assert.NoError(t, err)
decodedReq := decoded.(FetchAttestationResultReq)
assert.Equal(t, attestation.SNP, decodedReq.AttType)
}
func TestDecodeAttestationResultRequestWithInvalidNonce(t *testing.T) {
// Test with token nonce too long
tokenNonce := make([]byte, vtpm.Nonce+1)
req := &agent.AttestationResultRequest{TokenNonce: tokenNonce}
_, err := decodeAttestationResultRequest(context.Background(), req)
assert.Error(t, err)
assert.Equal(t, ErrVTPMNonceLength, err)
}
func TestEncodeAttestationResultResponse(t *testing.T) {
encoded, err := encodeAttestationResultResponse(context.Background(), fetchAttestationResultRes{File: []byte("attestation")})
assert.NoError(t, err)
assert.Equal(t, &agent.AttestationResultResponse{File: []byte("attestation")}, encoded)
}
func TestDecodeAttestationResultRequest(t *testing.T) {
nonce := [vtpm.Nonce]byte{}
req := &agent.AttestationResultRequest{TokenNonce: nonce[:]}
decoded, err := decodeAttestationResultRequest(context.Background(), req)
func TestDecodeIMAMeasurementsRequest(t *testing.T) {
decoded, err := decodeIMAMeasurementsRequest(context.Background(), &agent.IMAMeasurementsRequest{})
assert.NoError(t, err)
assert.Equal(t, FetchAttestationResultReq{tokenNonce: nonce}, decoded)
assert.Equal(t, imaMeasurementsReq{}, decoded)
}
func TestEncodeIMAMeasurementsResponse(t *testing.T) {
encoded, err := encodeIMAMeasurementsResponse(context.Background(), imaMeasurementsRes{
File: []byte("ima"),
PCR10: []byte("pcr10"),
})
assert.NoError(t, err)
assert.Equal(t, &agent.IMAMeasurementsResponse{
File: []byte("ima"),
Pcr10: []byte("pcr10"),
}, encoded)
}
func TestAlgoWithStreamError(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
mockStream := &MockAgentService_AlgoServer{ctx: context.Background()}
mockStream.On("Recv").Return(&agent.AlgoRequest{}, assert.AnError).Once()
err := server.Algo(mockStream)
assert.Error(t, err)
mockStream.AssertExpectations(t)
}
func TestDataWithStreamError(t *testing.T) {
mockService := new(mocks.Service)
server := NewServer(mockService)
mockStream := &MockAgentService_DataServer{ctx: context.Background()}
mockStream.On("Recv").Return(&agent.DataRequest{}, assert.AnError).Once()
err := server.Data(mockStream)
assert.Error(t, err)
mockStream.AssertExpectations(t)
}
+19
View File
@@ -4,6 +4,7 @@ package grpc
import (
"context"
"encoding/json"
"log/slog"
"sync"
"time"
@@ -13,6 +14,8 @@ import (
"github.com/ultravioletrs/cocos/agent/cvms"
"github.com/ultravioletrs/cocos/agent/cvms/api/grpc/storage"
"github.com/ultravioletrs/cocos/agent/cvms/server"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
"golang.org/x/sync/errgroup"
"google.golang.org/protobuf/proto"
@@ -249,6 +252,8 @@ func (client *CVMSClient) executeRun(ctx context.Context, runReq *cvms.Computati
return
}
ccPlatform := attestation.CCPlatform()
client.mu.Lock()
defer client.mu.Unlock()
@@ -274,6 +279,20 @@ func (client *CVMSClient) executeRun(ctx context.Context, runReq *cvms.Computati
runRes.RunRes.Error = err.Error()
}
defer func() {
if ccPlatform == attestation.Azure || ccPlatform == attestation.SNPvTPM {
cmpJson, err := json.Marshal(ac)
if err != nil {
client.logger.Error(err.Error())
return
}
if err = vtpm.ExtendPCR(vtpm.PCR16, cmpJson); err != nil {
client.logger.Error(err.Error())
return
}
}
}()
client.sendMessage(&cvms.ClientStreamMessage{Message: runRes})
}
+47 -5
View File
@@ -15,6 +15,7 @@ import (
"github.com/ultravioletrs/cocos/agent/mocks"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
clientmocks "github.com/ultravioletrs/cocos/pkg/clients/grpc/mocks"
"golang.org/x/crypto/sha3"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
@@ -37,13 +38,13 @@ func (m *mockStream) Send(msg *cvms.ClientStreamMessage) error {
func TestManagerClient_Process(t *testing.T) {
tests := []struct {
name string
setupMocks func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer)
setupMocks func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client)
expectError bool
errorMsg string
}{
{
name: "Stop computation",
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer) {
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client) {
mockStream.On("Recv").Return(&cvms.ServerStreamMessage{
Message: &cvms.ServerStreamMessage_StopComputation{
StopComputation: &cvms.StopComputation{},
@@ -58,7 +59,7 @@ func TestManagerClient_Process(t *testing.T) {
},
{
name: "Run request chunks",
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer) {
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client) {
mockStream.On("Recv").Return(&cvms.ServerStreamMessage{
Message: &cvms.ServerStreamMessage_RunReqChunks{
RunReqChunks: &cvms.RunReqChunks{},
@@ -69,9 +70,37 @@ func TestManagerClient_Process(t *testing.T) {
},
expectError: true,
},
{
name: "Agent state request",
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client) {
mockStream.On("Recv").Return(&cvms.ServerStreamMessage{
Message: &cvms.ServerStreamMessage_AgentStateReq{
AgentStateReq: &cvms.AgentStateReq{
Id: "test-agent",
},
},
}, nil)
mockStream.On("Send", mock.Anything).Return(nil)
mockSvc.On("State").Return("test-state")
},
expectError: true,
errorMsg: "context deadline exceeded",
},
{
name: "Disconnect request",
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client) {
mockStream.On("Recv").Return(&cvms.ServerStreamMessage{
Message: &cvms.ServerStreamMessage_DisconnectReq{},
}, nil)
mockStream.On("Send", mock.Anything).Return(nil)
grpcClient.On("Close").Return(nil)
},
expectError: true,
errorMsg: "context deadline exceeded",
},
{
name: "Receive error",
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer) {
setupMocks: func(mockStream *mockStream, mockSvc *mocks.Service, mockServerSvc *servermocks.AgentServer, grpcClient *clientmocks.Client) {
mockStream.On("Recv").Return(&cvms.ServerStreamMessage{}, assert.AnError)
},
expectError: true,
@@ -98,7 +127,7 @@ func TestManagerClient_Process(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
tc.setupMocks(mockStream, mockSvc, mockServerSvc)
tc.setupMocks(mockStream, mockSvc, mockServerSvc, grpcClient)
err = client.Process(ctx, cancel)
@@ -127,6 +156,19 @@ func TestManagerClient_handleRunReqChunks(t *testing.T) {
runReq := &cvms.ComputationRunReq{
Id: "test-id",
Datasets: []*cvms.Dataset{
{
Hash: sha3.New256().Sum([]byte("test-dataset")),
},
},
Algorithm: &cvms.Algorithm{
Hash: sha3.New256().Sum([]byte("test-algorithm")),
},
ResultConsumers: []*cvms.ResultConsumer{
{
UserKey: []byte("test-consumer"),
},
},
}
runReqBytes, _ := proto.Marshal(runReq)
+450
View File
@@ -0,0 +1,450 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package storage
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent/cvms"
)
func createTempDir(t *testing.T) string {
tmpDir, err := os.MkdirTemp("", "storage_test_*")
require.NoError(t, err)
t.Cleanup(func() {
os.RemoveAll(tmpDir)
})
return tmpDir
}
func createTestMessage(content string) *cvms.ClientStreamMessage {
return &cvms.ClientStreamMessage{
Message: &cvms.ClientStreamMessage_RunRes{
RunRes: &cvms.RunResponse{
Error: "",
ComputationId: content,
},
},
}
}
func TestNewFileStorage(t *testing.T) {
tests := []struct {
name string
storageDir string
expectError bool
}{
{
name: "valid directory",
storageDir: createTempDir(t),
expectError: false,
},
{
name: "non-existent directory gets created",
storageDir: filepath.Join(createTempDir(t), "subdir"),
expectError: false,
},
{
name: "invalid directory path",
storageDir: "/invalid/path/that/cannot/be/created",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
storage, err := NewFileStorage(tt.storageDir)
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, storage)
} else {
assert.NoError(t, err)
assert.NotNil(t, storage)
assert.Equal(t, filepath.Join(tt.storageDir, "pending_messages.json"), storage.path)
assert.Empty(t, storage.msgs)
}
})
}
}
func TestFileStorage_Load(t *testing.T) {
tests := []struct {
name string
setupFile func(string) error
expectedMsgs int
expectError bool
}{
{
name: "load from non-existent file",
setupFile: func(path string) error {
// Don't create file
return nil
},
expectedMsgs: 0,
expectError: false,
},
{
name: "load from empty file",
setupFile: func(path string) error {
return os.WriteFile(path, []byte("[]"), 0o644)
},
expectedMsgs: 0,
expectError: false,
},
{
name: "load from corrupted file",
setupFile: func(path string) error {
return os.WriteFile(path, []byte("invalid json"), 0o644)
},
expectedMsgs: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
err = tt.setupFile(storage.path)
require.NoError(t, err)
msgs, err := storage.Load()
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Len(t, msgs, tt.expectedMsgs)
}
})
}
}
func TestFileStorage_Save(t *testing.T) {
tests := []struct {
name string
messages []Message
expectError bool
}{
{
name: "save empty messages",
messages: []Message{},
expectError: false,
},
{
name: "save single message",
messages: []Message{
{
Message: createTestMessage("test"),
Time: time.Now(),
},
},
expectError: false,
},
{
name: "save multiple messages",
messages: []Message{
{
Message: createTestMessage("test1"),
Time: time.Now(),
},
{
Message: createTestMessage("test2"),
Time: time.Now().Add(time.Second),
},
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
err = storage.Save(tt.messages)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// Verify file was written correctly
_, err := os.ReadFile(storage.path)
assert.NoError(t, err)
// Verify internal state was updated
assert.Equal(t, tt.messages, storage.msgs)
}
})
}
}
func TestFileStorage_Add(t *testing.T) {
tests := []struct {
name string
initialMsgs []Message
newMessage *cvms.ClientStreamMessage
expectError bool
expectedCount int
}{
{
name: "add to empty storage",
initialMsgs: []Message{},
newMessage: createTestMessage("new"),
expectError: false,
expectedCount: 1,
},
{
name: "add to existing messages",
initialMsgs: []Message{
{
Message: createTestMessage("existing"),
Time: time.Now(),
},
},
newMessage: createTestMessage("new"),
expectError: false,
expectedCount: 2,
},
{
name: "add nil message",
initialMsgs: []Message{},
newMessage: nil,
expectError: false,
expectedCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Setup initial messages
if len(tt.initialMsgs) > 0 {
err = storage.Save(tt.initialMsgs)
require.NoError(t, err)
}
beforeTime := time.Now()
err = storage.Add(tt.newMessage)
afterTime := time.Now()
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// Verify message was added to internal state
assert.Len(t, storage.msgs, tt.expectedCount)
// Verify timestamp is reasonable
if tt.expectedCount > 0 {
lastMsg := storage.msgs[len(storage.msgs)-1]
assert.True(t, lastMsg.Time.After(beforeTime) || lastMsg.Time.Equal(beforeTime))
assert.True(t, lastMsg.Time.Before(afterTime) || lastMsg.Time.Equal(afterTime))
assert.Equal(t, tt.newMessage, lastMsg.Message)
}
_, err := os.ReadFile(storage.path)
assert.NoError(t, err)
}
})
}
}
func TestFileStorage_Clear(t *testing.T) {
tests := []struct {
name string
initialMsgs []Message
expectError bool
}{
{
name: "clear empty storage",
initialMsgs: []Message{},
expectError: false,
},
{
name: "clear storage with messages",
initialMsgs: []Message{
{
Message: createTestMessage("test1"),
Time: time.Now(),
},
{
Message: createTestMessage("test2"),
Time: time.Now(),
},
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Setup initial messages
if len(tt.initialMsgs) > 0 {
err = storage.Save(tt.initialMsgs)
require.NoError(t, err)
}
err = storage.Clear()
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// Verify internal state is cleared
assert.Empty(t, storage.msgs)
// Verify file contains empty array
data, err := os.ReadFile(storage.path)
assert.NoError(t, err)
assert.Equal(t, "[]", string(data))
}
})
}
}
func TestFileStorage_ConcurrentAccess(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Test concurrent Add operations
numGoroutines := 10
done := make(chan bool, numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer func() { done <- true }()
msg := createTestMessage(string(rune('A' + id)))
err := storage.Add(msg)
assert.NoError(t, err)
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < numGoroutines; i++ {
<-done
}
// Verify all messages were added
msgs, err := storage.Load()
assert.NoError(t, err)
assert.Len(t, msgs, numGoroutines)
}
func TestFileStorage_IntegrationFlow(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Test full workflow
// 1. Load from empty storage
msgs, err := storage.Load()
assert.NoError(t, err)
assert.Empty(t, msgs)
// 2. Add some messages
msg1 := createTestMessage("message1")
err = storage.Add(msg1)
assert.NoError(t, err)
msg2 := createTestMessage("message2")
err = storage.Add(msg2)
assert.NoError(t, err)
// 3. Load and verify
msgs, err = storage.Load()
assert.NoError(t, err)
assert.Len(t, msgs, 2)
// 4. Save new set of messages
newMsgs := []Message{
{
Message: createTestMessage("new1"),
Time: time.Now(),
},
}
err = storage.Save(newMsgs)
assert.NoError(t, err)
// 5. Load and verify replacement
msgs, err = storage.Load()
assert.NoError(t, err)
assert.Len(t, msgs, 1)
// 6. Clear storage
err = storage.Clear()
assert.NoError(t, err)
// 7. Verify empty
msgs, err = storage.Load()
assert.NoError(t, err)
assert.Empty(t, msgs)
}
func TestFileStorage_FilePermissions(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Add a message to create the file
msg := createTestMessage("test")
err = storage.Add(msg)
assert.NoError(t, err)
// Check file permissions
info, err := os.Stat(storage.path)
assert.NoError(t, err)
assert.Equal(t, os.FileMode(0o644), info.Mode().Perm())
}
func TestFileStorage_ErrorHandling(t *testing.T) {
tmpDir := createTempDir(t)
storage, err := NewFileStorage(tmpDir)
require.NoError(t, err)
// Make directory read-only to trigger write errors
err = os.Chmod(tmpDir, 0o555)
require.NoError(t, err)
// Restore permissions for cleanup
t.Cleanup(func() {
if err := os.Chmod(tmpDir, 0o755); err != nil {
t.Errorf("Failed to restore permissions: %v", err)
}
})
// Try to add a message - should fail due to write permissions
msg := createTestMessage("test")
err = storage.Add(msg)
assert.Error(t, err)
// Try to save - should fail due to write permissions
err = storage.Save([]Message{})
assert.Error(t, err)
// Try to clear - should fail due to write permissions
err = storage.Clear()
assert.Error(t, err)
}
+544
View File
@@ -0,0 +1,544 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package server
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"log/slog"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/agent/mocks"
)
func setupTest(t *testing.T) (*slog.Logger, *mocks.Service, string, string, string, []byte) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
mockSvc := new(mocks.Service)
host := "localhost"
caUrl := "https://ca.example.com"
cvmId := "test-cvm-id"
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
assert.NoError(t, err, "Failed to generate ECDSA key")
pubkey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
assert.NoError(t, err, "Failed to marshal public key")
return logger, mockSvc, host, caUrl, cvmId, pubkey
}
func TestNewServer(t *testing.T) {
logger, svc, host, caUrl, cvmId, _ := setupTest(t)
tests := []struct {
name string
logger *slog.Logger
svc agent.Service
host string
caUrl string
cvmId string
expected AgentServer
}{
{
name: "valid server creation",
logger: logger,
svc: svc,
host: host,
caUrl: caUrl,
cvmId: cvmId,
},
{
name: "server with empty host",
logger: logger,
svc: svc,
host: "",
caUrl: caUrl,
cvmId: cvmId,
},
{
name: "server with empty caUrl",
logger: logger,
svc: svc,
host: host,
caUrl: "",
cvmId: cvmId,
},
{
name: "server with empty cvmId",
logger: logger,
svc: svc,
host: host,
caUrl: caUrl,
cvmId: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := NewServer(tt.logger, tt.svc, tt.host, tt.caUrl, tt.cvmId)
assert.NotNil(t, server)
agentSrv, ok := server.(*agentServer)
assert.True(t, ok)
assert.Equal(t, tt.logger, agentSrv.logger)
assert.Equal(t, tt.svc, agentSrv.svc)
assert.Equal(t, tt.host, agentSrv.host)
assert.Equal(t, tt.caUrl, agentSrv.caUrl)
assert.Equal(t, tt.cvmId, agentSrv.cvmId)
})
}
}
func TestAgentServer_Start(t *testing.T) {
logger, svc, host, caUrl, cvmId, pubKey := setupTest(t)
tests := []struct {
name string
cfg agent.AgentConfig
cmp agent.Computation
setupMocks func(*mocks.Service)
expectedError bool
errorContains string
}{
{
name: "successful start with default port",
cfg: agent.AgentConfig{
Port: "",
CertFile: "cert.pem",
KeyFile: "key.pem",
ServerCAFile: "server-ca.pem",
ClientCAFile: "client-ca.pem",
AttestedTls: true,
},
cmp: agent.Computation{
ID: "test-computation-1",
Name: "Test Computation",
Description: "A test computation",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x01, 0x02, 0x03},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x04, 0x05, 0x06},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
},
setupMocks: func(m *mocks.Service) {
},
expectedError: false,
},
{
name: "successful start with custom port",
cfg: agent.AgentConfig{
Port: "8080",
CertFile: "cert.pem",
KeyFile: "key.pem",
ServerCAFile: "server-ca.pem",
ClientCAFile: "client-ca.pem",
AttestedTls: false,
},
cmp: agent.Computation{
ID: "test-computation-2",
Name: "Test Computation 2",
Description: "Another test computation",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x07, 0x08, 0x09},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x0a, 0x0b, 0x0c},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
},
setupMocks: func(m *mocks.Service) {
},
expectedError: false,
},
{
name: "start with minimal config",
cfg: agent.AgentConfig{
Port: "9090",
AttestedTls: false,
},
cmp: agent.Computation{
ID: "test-computation-3",
Name: "Minimal Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x0d, 0x0e, 0x0f},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x10, 0x11, 0x12},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
},
setupMocks: func(m *mocks.Service) {
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupMocks(svc)
server := NewServer(logger, svc, host, caUrl, cvmId)
err := server.Start(tt.cfg, tt.cmp)
if tt.expectedError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
// Verify the port was set correctly
agentSrv := server.(*agentServer)
assert.NotNil(t, agentSrv.gs)
if err := server.Stop(); err != nil {
t.Fatalf("Failed to stop server after start: %v", err)
}
}
svc.AssertExpectations(t)
})
}
}
func TestAgentServer_Stop(t *testing.T) {
logger, svc, host, caUrl, cvmId, pubKey := setupTest(t)
tests := []struct {
name string
setupServer func(AgentServer) error
expectedError bool
errorContains string
}{
{
name: "stop unstarted server",
setupServer: func(server AgentServer) error {
// Don't start the server
return nil
},
expectedError: false,
},
{
name: "stop started server",
setupServer: func(server AgentServer) error {
cfg := agent.AgentConfig{
Port: "7004",
}
cmp := agent.Computation{
ID: "test-stop-computation",
Name: "Stop Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x19, 0x1a, 0x1b},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x1c, 0x1d, 0x1e},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
}
return server.Start(cfg, cmp)
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := NewServer(logger, svc, host, caUrl, cvmId)
err := tt.setupServer(server)
if err != nil {
t.Fatalf("Setup failed: %v", err)
}
// Give the server a moment to start if it was started
time.Sleep(10 * time.Millisecond)
err = server.Stop()
if tt.expectedError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
svc.AssertExpectations(t)
})
}
}
func TestAgentServer_StopMultipleTimes(t *testing.T) {
logger, svc, host, caUrl, cvmId, pubKey := setupTest(t)
server := NewServer(logger, svc, host, caUrl, cvmId)
// Start the server
cfg := agent.AgentConfig{Port: "7005"}
cmp := agent.Computation{
ID: "test-multiple-stop",
Name: "Multiple Stop Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x1f, 0x20, 0x21},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x22, 0x23, 0x24},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
}
err := server.Start(cfg, cmp)
assert.NoError(t, err)
// Give the server a moment to start
time.Sleep(10 * time.Millisecond)
// Stop the server multiple times
err1 := server.Stop()
err2 := server.Stop()
err3 := server.Stop()
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
svc.AssertExpectations(t)
}
func TestAgentServer_StartAfterStop(t *testing.T) {
logger, svc, host, caUrl, cvmId, pubKey := setupTest(t)
server := NewServer(logger, svc, host, caUrl, cvmId)
cfg := agent.AgentConfig{Port: "7006"}
cmp := agent.Computation{
ID: "test-restart",
Name: "Restart Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x25, 0x26, 0x27},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x28, 0x29, 0x2a},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
}
// Start, stop, then start again
err := server.Start(cfg, cmp)
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
err = server.Stop()
assert.NoError(t, err)
// Start again with different config
cfg2 := agent.AgentConfig{Port: "7007"}
cmp2 := agent.Computation{
ID: "test-restart-2",
Name: "Restart Test 2",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x2b, 0x2c, 0x2d},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x2e, 0x2f, 0x30},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
}
err = server.Start(cfg2, cmp2)
assert.NoError(t, err)
time.Sleep(10 * time.Millisecond)
err = server.Stop()
assert.NoError(t, err)
svc.AssertExpectations(t)
}
func TestAgentServer_ConfigValidation(t *testing.T) {
logger, svc, host, caUrl, cvmId, pubKey := setupTest(t)
tests := []struct {
name string
config agent.AgentConfig
cmp agent.Computation
valid bool
}{
{
name: "valid config with all fields",
config: agent.AgentConfig{
Port: "8080",
CertFile: "cert.pem",
KeyFile: "key.pem",
ServerCAFile: "server-ca.pem",
ClientCAFile: "client-ca.pem",
AttestedTls: true,
},
cmp: agent.Computation{
ID: "valid-config-test",
Name: "Valid Config Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x31, 0x32, 0x33},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x34, 0x35, 0x36},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
},
valid: true,
},
{
name: "valid config with minimal fields",
config: agent.AgentConfig{
Port: "9090",
},
cmp: agent.Computation{
ID: "minimal-config-test",
Name: "Minimal Config Test",
Algorithm: agent.Algorithm{
Hash: [32]byte{0x37, 0x38, 0x39},
UserKey: pubKey,
},
Datasets: []agent.Dataset{
{
Hash: [32]byte{0x3a, 0x3b, 0x3c},
UserKey: pubKey,
},
},
ResultConsumers: []agent.ResultConsumer{
{
UserKey: pubKey,
},
},
},
valid: true,
},
{
name: "config with empty port uses default",
config: agent.AgentConfig{
Port: "",
},
cmp: agent.Computation{
ID: "default-port-test",
Name: "Default Port Test",
Algorithm: agent.Algorithm{Hash: [32]byte{0x3d, 0x3e, 0x3f}, UserKey: pubKey},
Datasets: []agent.Dataset{
{Hash: [32]byte{0x40, 0x41, 0x42}, UserKey: pubKey},
},
ResultConsumers: []agent.ResultConsumer{
{UserKey: pubKey},
},
},
valid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := NewServer(logger, svc, host, caUrl, cvmId)
err := server.Start(tt.config, tt.cmp)
if tt.valid {
assert.NoError(t, err)
// Verify default port is used when empty
if tt.config.Port == "" {
agentSrv := server.(*agentServer)
assert.NotNil(t, agentSrv.gs)
}
time.Sleep(10 * time.Millisecond)
if err := server.Stop(); err != nil {
t.Fatalf("Failed to stop server after start: %v", err)
}
} else {
assert.Error(t, err)
}
svc.AssertExpectations(t)
})
}
}
func TestConstants(t *testing.T) {
assert.Equal(t, "agent", svcName)
assert.Equal(t, "7002", defSvcGRPCPort)
}
+388
View File
@@ -0,0 +1,388 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery v2.53.3. DO NOT EDIT.
package mocks
import (
context "context"
agent "github.com/ultravioletrs/cocos/agent"
metadata "google.golang.org/grpc/metadata"
mock "github.com/stretchr/testify/mock"
)
// AgentService_IMAMeasurementsClient is an autogenerated mock type for the AgentService_IMAMeasurementsClient type
type AgentService_IMAMeasurementsClient[Res interface{}] struct {
mock.Mock
}
type AgentService_IMAMeasurementsClient_Expecter[Res interface{}] struct {
mock *mock.Mock
}
func (_m *AgentService_IMAMeasurementsClient[Res]) EXPECT() *AgentService_IMAMeasurementsClient_Expecter[Res] {
return &AgentService_IMAMeasurementsClient_Expecter[Res]{mock: &_m.Mock}
}
// CloseSend provides a mock function with no fields
func (_m *AgentService_IMAMeasurementsClient[Res]) CloseSend() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for CloseSend")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// AgentService_IMAMeasurementsClient_CloseSend_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CloseSend'
type AgentService_IMAMeasurementsClient_CloseSend_Call[Res interface{}] struct {
*mock.Call
}
// CloseSend is a helper method to define mock.On call
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) CloseSend() *AgentService_IMAMeasurementsClient_CloseSend_Call[Res] {
return &AgentService_IMAMeasurementsClient_CloseSend_Call[Res]{Call: _e.mock.On("CloseSend")}
}
func (_c *AgentService_IMAMeasurementsClient_CloseSend_Call[Res]) Run(run func()) *AgentService_IMAMeasurementsClient_CloseSend_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_CloseSend_Call[Res]) Return(_a0 error) *AgentService_IMAMeasurementsClient_CloseSend_Call[Res] {
_c.Call.Return(_a0)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_CloseSend_Call[Res]) RunAndReturn(run func() error) *AgentService_IMAMeasurementsClient_CloseSend_Call[Res] {
_c.Call.Return(run)
return _c
}
// Context provides a mock function with no fields
func (_m *AgentService_IMAMeasurementsClient[Res]) Context() context.Context {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Context")
}
var r0 context.Context
if rf, ok := ret.Get(0).(func() context.Context); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(context.Context)
}
}
return r0
}
// AgentService_IMAMeasurementsClient_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context'
type AgentService_IMAMeasurementsClient_Context_Call[Res interface{}] struct {
*mock.Call
}
// Context is a helper method to define mock.On call
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) Context() *AgentService_IMAMeasurementsClient_Context_Call[Res] {
return &AgentService_IMAMeasurementsClient_Context_Call[Res]{Call: _e.mock.On("Context")}
}
func (_c *AgentService_IMAMeasurementsClient_Context_Call[Res]) Run(run func()) *AgentService_IMAMeasurementsClient_Context_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Context_Call[Res]) Return(_a0 context.Context) *AgentService_IMAMeasurementsClient_Context_Call[Res] {
_c.Call.Return(_a0)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Context_Call[Res]) RunAndReturn(run func() context.Context) *AgentService_IMAMeasurementsClient_Context_Call[Res] {
_c.Call.Return(run)
return _c
}
// Header provides a mock function with no fields
func (_m *AgentService_IMAMeasurementsClient[Res]) Header() (metadata.MD, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Header")
}
var r0 metadata.MD
var r1 error
if rf, ok := ret.Get(0).(func() (metadata.MD, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() metadata.MD); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(metadata.MD)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AgentService_IMAMeasurementsClient_Header_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Header'
type AgentService_IMAMeasurementsClient_Header_Call[Res interface{}] struct {
*mock.Call
}
// Header is a helper method to define mock.On call
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) Header() *AgentService_IMAMeasurementsClient_Header_Call[Res] {
return &AgentService_IMAMeasurementsClient_Header_Call[Res]{Call: _e.mock.On("Header")}
}
func (_c *AgentService_IMAMeasurementsClient_Header_Call[Res]) Run(run func()) *AgentService_IMAMeasurementsClient_Header_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Header_Call[Res]) Return(_a0 metadata.MD, _a1 error) *AgentService_IMAMeasurementsClient_Header_Call[Res] {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Header_Call[Res]) RunAndReturn(run func() (metadata.MD, error)) *AgentService_IMAMeasurementsClient_Header_Call[Res] {
_c.Call.Return(run)
return _c
}
// Recv provides a mock function with no fields
func (_m *AgentService_IMAMeasurementsClient[Res]) Recv() (*agent.IMAMeasurementsResponse, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Recv")
}
var r0 *agent.IMAMeasurementsResponse
var r1 error
if rf, ok := ret.Get(0).(func() (*agent.IMAMeasurementsResponse, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *agent.IMAMeasurementsResponse); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*agent.IMAMeasurementsResponse)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AgentService_IMAMeasurementsClient_Recv_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Recv'
type AgentService_IMAMeasurementsClient_Recv_Call[Res interface{}] struct {
*mock.Call
}
// Recv is a helper method to define mock.On call
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) Recv() *AgentService_IMAMeasurementsClient_Recv_Call[Res] {
return &AgentService_IMAMeasurementsClient_Recv_Call[Res]{Call: _e.mock.On("Recv")}
}
func (_c *AgentService_IMAMeasurementsClient_Recv_Call[Res]) Run(run func()) *AgentService_IMAMeasurementsClient_Recv_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Recv_Call[Res]) Return(_a0 *agent.IMAMeasurementsResponse, _a1 error) *AgentService_IMAMeasurementsClient_Recv_Call[Res] {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Recv_Call[Res]) RunAndReturn(run func() (*agent.IMAMeasurementsResponse, error)) *AgentService_IMAMeasurementsClient_Recv_Call[Res] {
_c.Call.Return(run)
return _c
}
// RecvMsg provides a mock function with given fields: m
func (_m *AgentService_IMAMeasurementsClient[Res]) RecvMsg(m interface{}) error {
ret := _m.Called(m)
if len(ret) == 0 {
panic("no return value specified for RecvMsg")
}
var r0 error
if rf, ok := ret.Get(0).(func(interface{}) error); ok {
r0 = rf(m)
} else {
r0 = ret.Error(0)
}
return r0
}
// AgentService_IMAMeasurementsClient_RecvMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RecvMsg'
type AgentService_IMAMeasurementsClient_RecvMsg_Call[Res interface{}] struct {
*mock.Call
}
// RecvMsg is a helper method to define mock.On call
// - m interface{}
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) RecvMsg(m interface{}) *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res] {
return &AgentService_IMAMeasurementsClient_RecvMsg_Call[Res]{Call: _e.mock.On("RecvMsg", m)}
}
func (_c *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res]) Run(run func(m interface{})) *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(interface{}))
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res]) Return(_a0 error) *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res] {
_c.Call.Return(_a0)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res]) RunAndReturn(run func(interface{}) error) *AgentService_IMAMeasurementsClient_RecvMsg_Call[Res] {
_c.Call.Return(run)
return _c
}
// SendMsg provides a mock function with given fields: m
func (_m *AgentService_IMAMeasurementsClient[Res]) SendMsg(m interface{}) error {
ret := _m.Called(m)
if len(ret) == 0 {
panic("no return value specified for SendMsg")
}
var r0 error
if rf, ok := ret.Get(0).(func(interface{}) error); ok {
r0 = rf(m)
} else {
r0 = ret.Error(0)
}
return r0
}
// AgentService_IMAMeasurementsClient_SendMsg_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMsg'
type AgentService_IMAMeasurementsClient_SendMsg_Call[Res interface{}] struct {
*mock.Call
}
// SendMsg is a helper method to define mock.On call
// - m interface{}
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) SendMsg(m interface{}) *AgentService_IMAMeasurementsClient_SendMsg_Call[Res] {
return &AgentService_IMAMeasurementsClient_SendMsg_Call[Res]{Call: _e.mock.On("SendMsg", m)}
}
func (_c *AgentService_IMAMeasurementsClient_SendMsg_Call[Res]) Run(run func(m interface{})) *AgentService_IMAMeasurementsClient_SendMsg_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(interface{}))
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_SendMsg_Call[Res]) Return(_a0 error) *AgentService_IMAMeasurementsClient_SendMsg_Call[Res] {
_c.Call.Return(_a0)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_SendMsg_Call[Res]) RunAndReturn(run func(interface{}) error) *AgentService_IMAMeasurementsClient_SendMsg_Call[Res] {
_c.Call.Return(run)
return _c
}
// Trailer provides a mock function with no fields
func (_m *AgentService_IMAMeasurementsClient[Res]) Trailer() metadata.MD {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Trailer")
}
var r0 metadata.MD
if rf, ok := ret.Get(0).(func() metadata.MD); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(metadata.MD)
}
}
return r0
}
// AgentService_IMAMeasurementsClient_Trailer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Trailer'
type AgentService_IMAMeasurementsClient_Trailer_Call[Res interface{}] struct {
*mock.Call
}
// Trailer is a helper method to define mock.On call
func (_e *AgentService_IMAMeasurementsClient_Expecter[Res]) Trailer() *AgentService_IMAMeasurementsClient_Trailer_Call[Res] {
return &AgentService_IMAMeasurementsClient_Trailer_Call[Res]{Call: _e.mock.On("Trailer")}
}
func (_c *AgentService_IMAMeasurementsClient_Trailer_Call[Res]) Run(run func()) *AgentService_IMAMeasurementsClient_Trailer_Call[Res] {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Trailer_Call[Res]) Return(_a0 metadata.MD) *AgentService_IMAMeasurementsClient_Trailer_Call[Res] {
_c.Call.Return(_a0)
return _c
}
func (_c *AgentService_IMAMeasurementsClient_Trailer_Call[Res]) RunAndReturn(run func() metadata.MD) *AgentService_IMAMeasurementsClient_Trailer_Call[Res] {
_c.Call.Return(run)
return _c
}
// NewAgentService_IMAMeasurementsClient creates a new instance of AgentService_IMAMeasurementsClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewAgentService_IMAMeasurementsClient[Res interface{}](t interface {
mock.TestingT
Cleanup(func())
}) *AgentService_IMAMeasurementsClient[Res] {
mock := &AgentService_IMAMeasurementsClient[Res]{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+12 -3
View File
@@ -12,6 +12,7 @@ import (
"path/filepath"
"slices"
sync "sync"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/ultravioletrs/cocos/agent/algorithm"
@@ -183,8 +184,12 @@ func New(ctx context.Context, logger *slog.Logger, eventSvc events.Service, prov
logger.Error(err.Error())
}
}()
time.Sleep(100 * time.Millisecond)
sm.SendEvent(Start)
time.Sleep(100 * time.Millisecond)
return svc
}
@@ -257,8 +262,12 @@ func (as *agentService) StopComputation(ctx context.Context) error {
as.logger.Error(err.Error())
}
}()
time.Sleep(100 * time.Millisecond)
as.sm.SendEvent(Start)
time.Sleep(100 * time.Millisecond)
return nil
}
@@ -427,7 +436,7 @@ func (as *agentService) Result(ctx context.Context) ([]byte, error) {
func (as *agentService) Attestation(ctx context.Context, reportData [quoteprovider.Nonce]byte, nonce [vtpm.Nonce]byte, attType attestation.PlatformType) ([]byte, error) {
switch attType {
case attestation.SNP:
case attestation.SNP, attestation.TDX:
rawQuote, err := as.provider.TeeAttestation(reportData[:])
if err != nil {
return []byte{}, errors.Wrap(ErrAttestationFailed, err)
@@ -520,12 +529,12 @@ func (as *agentService) publishEvent(status string) statemachine.Action {
func (as *agentService) IMAMeasurements(ctx context.Context) ([]byte, []byte, error) {
data, err := os.ReadFile(ImaMeasurementsFilePath)
if err != nil {
return nil, nil, fmt.Errorf("Error reading Linux IMA measurements file: %s", err.Error())
return nil, nil, fmt.Errorf("error reading Linux IMA measurements file: %s", err.Error())
}
pcr10, err := vtpm.GetPCRSHA1Value(ImaPcrIndex)
if err != nil {
return nil, nil, fmt.Errorf("Error reading TPM PCR #10: %s", err.Error())
return nil, nil, fmt.Errorf("error reading TPM PCR #10: %s", err.Error())
}
return data, pcr10, nil
+196 -1
View File
@@ -5,8 +5,10 @@ package agent
import (
"context"
"crypto/rand"
"fmt"
"log"
"os"
"path/filepath"
"testing"
"time"
@@ -16,6 +18,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent/algorithm"
algomocks "github.com/ultravioletrs/cocos/agent/algorithm/mocks"
"github.com/ultravioletrs/cocos/agent/algorithm/python"
"github.com/ultravioletrs/cocos/agent/events/mocks"
"github.com/ultravioletrs/cocos/agent/statemachine"
@@ -41,6 +44,7 @@ func TestAlgo(t *testing.T) {
require.NoError(t, err)
algoHash := sha3.Sum256(algo)
vtpm.ExternalTPM = &vtpm.DummyRWC{}
reqFile, err := os.ReadFile(reqPath)
require.NoError(t, err)
@@ -142,6 +146,7 @@ func TestData(t *testing.T) {
require.NoError(t, err)
algoHash := sha3.Sum256(algo)
vtpm.ExternalTPM = &vtpm.DummyRWC{}
alg := Algorithm{
Hash: algoHash,
@@ -387,14 +392,20 @@ func TestAttestation(t *testing.T) {
defer cancel()
getQuote := provider.On("TeeAttestation", mock.Anything).Return(tc.rawQuote, tc.err)
vtpmQuote := provider.On("VTpmAttestation", mock.Anything).Return(tc.rawQuote, tc.err)
snpVtpm := provider.On("Attestation", mock.Anything, mock.Anything).Return(tc.rawQuote, tc.err)
if tc.err != ErrAttestationFailed && tc.err != ErrAttestationVTpmFailed {
getQuote = provider.On("TeeAttestation", mock.Anything).Return(tc.nonce, nil)
vtpmQuote = provider.On("VTpmAttestation", mock.Anything).Return(tc.nonce[:], nil)
snpVtpm = provider.On("Attestation", mock.Anything, mock.Anything).Return(tc.nonce[:], nil)
}
defer getQuote.Unset()
defer vtpmQuote.Unset()
defer snpVtpm.Unset()
svc := New(ctx, mglog.NewMock(), events, provider, 0)
time.Sleep(300 * time.Millisecond)
_, err := svc.Attestation(ctx, tc.reportData, tc.nonce, 0)
_, err := svc.Attestation(ctx, tc.reportData, tc.nonce, tc.platform)
assert.True(t, errors.Contains(err, tc.err), "expected %v, got %v", tc.err, err)
})
}
@@ -481,3 +492,187 @@ func testComputation(t *testing.T) Computation {
ResultConsumers: []ResultConsumer{{UserKey: []byte("key")}},
}
}
func TestStopComputation(t *testing.T) {
testDataDir := "test_datasets"
testResultsDir := "test_results"
cases := []struct {
name string
setupDirs bool
setupAlgo bool
algoStopErr error
expectedErr error
}{
{
name: "Stop computation successfully",
setupDirs: true,
setupAlgo: true,
algoStopErr: nil,
expectedErr: nil,
},
{
name: "Stop computation with algorithm stop error",
setupDirs: true,
setupAlgo: true,
algoStopErr: fmt.Errorf("algorithm stop failed"),
expectedErr: fmt.Errorf("error stopping computation: algorithm stop failed"),
},
{
name: "Stop computation without algorithm",
setupDirs: true,
setupAlgo: false,
algoStopErr: nil,
expectedErr: nil,
},
{
name: "Stop computation with missing directories",
setupDirs: false,
setupAlgo: false,
algoStopErr: nil,
expectedErr: nil, // os.RemoveAll doesn't error on non-existing directories
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
events := new(mocks.Service)
events.On("SendEvent", mock.Anything, "Stopped", "Stopped", mock.Anything).Return()
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
svc := New(ctx, mglog.NewMock(), events, &attestation.EmptyProvider{}, 0).(*agentService)
svc.computation = Computation{
ID: "test-computation",
Name: "test",
}
if tc.setupDirs {
err := os.MkdirAll(testDataDir, 0o755)
require.NoError(t, err)
err = os.MkdirAll(testResultsDir, 0o755)
require.NoError(t, err)
}
if tc.setupAlgo {
mockAlgo := new(algomocks.Algorithm)
mockAlgo.On("Stop").Return(tc.algoStopErr)
svc.algorithm = mockAlgo
}
err := svc.StopComputation(ctx)
if tc.expectedErr != nil {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErr.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, ReceivingManifest, svc.sm.GetState())
assert.Nil(t, svc.result)
assert.Nil(t, svc.runError)
assert.False(t, svc.resultsConsumed)
events.AssertExpectations(t)
_ = os.RemoveAll(testDataDir)
_ = os.RemoveAll(testResultsDir)
})
}
}
func TestStopComputationIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
algo := []byte("#!/bin/bash\necho 'test algorithm'")
algoHash := sha3.Sum256(algo)
testDir := "test_integration"
err := os.MkdirAll(testDir, 0o755)
require.NoError(t, err)
defer os.RemoveAll(testDir)
algoFile := filepath.Join(testDir, "test_algo")
err = os.WriteFile(algoFile, algo, 0o755)
require.NoError(t, err)
events := new(mocks.Service)
events.On("SendEvent", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
ctx := metadata.NewIncomingContext(context.Background(),
metadata.Pairs(algorithm.AlgoTypeKey, "bin"),
)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
svc := New(ctx, mglog.NewMock(), events, &attestation.EmptyProvider{}, 0)
computation := Computation{
ID: "integration-test",
Name: "Integration Test",
Algorithm: Algorithm{
Hash: algoHash,
Algorithm: algo,
},
}
err = svc.InitComputation(ctx, computation)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
err = svc.Algo(ctx, Algorithm{
Hash: algoHash,
Algorithm: algo,
})
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
err = svc.StopComputation(ctx)
assert.NoError(t, err)
assert.Equal(t, "ReceivingManifest", svc.State())
}
func TestStopComputationConcurrent(t *testing.T) {
events := new(mocks.Service)
events.On("SendEvent", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
svc := New(ctx, mglog.NewMock(), events, &attestation.EmptyProvider{}, 0)
svc.(*agentService).computation = Computation{
ID: "concurrent-test",
Name: "Concurrent Test",
}
const numGoroutines = 10
errChan := make(chan error, numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
err := svc.StopComputation(ctx)
errChan <- err
}()
}
var errors []error
for i := 0; i < numGoroutines; i++ {
err := <-errChan
if err != nil {
errors = append(errors, err)
}
}
assert.True(t, len(errors) < numGoroutines, "All StopComputation calls failed")
}
+9 -3
View File
@@ -54,10 +54,11 @@ func TestAddTransition(t *testing.T) {
go func() {
if err := sm.Start(ctx); err != context.Canceled {
t.Errorf("Start returned error: %v", err)
}
}()
time.Sleep(50 * time.Millisecond)
sm.SendEvent(Event1)
time.Sleep(50 * time.Millisecond)
@@ -79,7 +80,7 @@ func TestSetAction(t *testing.T) {
sm.AddTransition(statemachine.Transition{From: State1, Event: Event1, To: State2})
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
defer cancel()
go func() {
@@ -88,8 +89,12 @@ func TestSetAction(t *testing.T) {
}
}()
time.Sleep(50 * time.Millisecond)
sm.SendEvent(Event1)
time.Sleep(50 * time.Millisecond)
wg.Wait()
if ctx.Err() != nil {
@@ -132,10 +137,11 @@ func TestMultipleTransitions(t *testing.T) {
go func() {
if err := sm.Start(ctx); err != context.Canceled {
t.Errorf("Start returned error: %v", err)
}
}()
time.Sleep(50 * time.Millisecond)
transitions := []struct {
event MockEvent
want MockState
+23 -3
View File
@@ -39,6 +39,7 @@ type stateMachine struct {
transitions map[State]map[Event]State
actions map[State]Action
eventChan chan Event
resetChan chan struct{}
}
func NewStateMachine(initialState State) StateMachine {
@@ -47,6 +48,7 @@ func NewStateMachine(initialState State) StateMachine {
transitions: make(map[State]map[Event]State),
actions: make(map[State]Action),
eventChan: make(chan Event),
resetChan: make(chan struct{}),
}
}
@@ -74,16 +76,31 @@ func (sm *stateMachine) GetState() State {
}
func (sm *stateMachine) SendEvent(event Event) {
sm.eventChan <- event
sm.mu.Lock()
eventChan := sm.eventChan
sm.mu.Unlock()
select {
case eventChan <- event:
default:
// Channel might be closed or full, ignore the event
}
}
func (sm *stateMachine) Start(ctx context.Context) error {
for {
sm.mu.Lock()
eventChan := sm.eventChan
resetChan := sm.resetChan
sm.mu.Unlock()
select {
case event := <-sm.eventChan:
case event := <-eventChan:
if err := sm.handleEvent(event); err != nil {
return err
}
case <-resetChan:
continue
case <-ctx.Done():
return ctx.Err()
}
@@ -100,8 +117,11 @@ func (sm *stateMachine) Reset(initialState State) {
// Close the existing event channel to stop processing events
close(sm.eventChan)
// Create a new event channel
// Close the reset channel to signal Start() to restart
close(sm.resetChan)
sm.eventChan = make(chan Event)
sm.resetChan = make(chan struct{})
}
func (sm *stateMachine) handleEvent(event Event) error {
+607
View File
@@ -0,0 +1,607 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package statemachine
import (
"context"
"sync"
"testing"
"time"
)
type testState string
func (s testState) String() string {
return string(s)
}
type testEvent string
func (e testEvent) String() string {
return string(e)
}
const (
StateIdle testState = "idle"
StateRunning testState = "running"
StatePaused testState = "paused"
StateStopped testState = "stopped"
StateError testState = "error"
)
const (
EventStart testEvent = "start"
EventPause testEvent = "pause"
EventStop testEvent = "stop"
EventReset testEvent = "reset"
EventError testEvent = "error"
)
func TestNewStateMachine(t *testing.T) {
tests := []struct {
name string
initialState State
want State
}{
{
name: "create with idle state",
initialState: StateIdle,
want: StateIdle,
},
{
name: "create with running state",
initialState: StateRunning,
want: StateRunning,
},
{
name: "create with custom state",
initialState: testState("custom"),
want: testState("custom"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(tt.initialState)
if got := sm.GetState(); got != tt.want {
t.Errorf("NewStateMachine() initial state = %v, want %v", got, tt.want)
}
})
}
}
func TestStateMachine_AddTransition(t *testing.T) {
tests := []struct {
name string
transitions []Transition
from State
event Event
expectTo State
expectValid bool
}{
{
name: "single transition",
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
from: StateIdle,
event: EventStart,
expectTo: StateRunning,
expectValid: true,
},
{
name: "multiple transitions from same state",
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
{From: StateIdle, Event: EventError, To: StateError},
},
from: StateIdle,
event: EventError,
expectTo: StateError,
expectValid: true,
},
{
name: "overwrite existing transition",
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
{From: StateIdle, Event: EventStart, To: StatePaused}, // Overwrite
},
from: StateIdle,
event: EventStart,
expectTo: StatePaused,
expectValid: true,
},
{
name: "transition not found",
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
from: StateRunning,
event: EventPause,
expectValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(StateIdle).(*stateMachine)
for _, transition := range tt.transitions {
sm.AddTransition(transition)
}
sm.mu.Lock()
nextState, valid := sm.transitions[tt.from][tt.event]
sm.mu.Unlock()
if valid != tt.expectValid {
t.Errorf("Transition validity = %v, want %v", valid, tt.expectValid)
}
if tt.expectValid && nextState != tt.expectTo {
t.Errorf("Transition destination = %v, want %v", nextState, tt.expectTo)
}
})
}
}
func TestStateMachine_SetAction(t *testing.T) {
tests := []struct {
name string
state State
action Action
expectAction bool
}{
{
name: "set action for state",
state: StateRunning,
action: func(s State) {
},
expectAction: true,
},
{
name: "set nil action",
state: StatePaused,
action: nil,
expectAction: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(StateIdle).(*stateMachine)
sm.SetAction(tt.state, tt.action)
sm.mu.Lock()
action := sm.actions[tt.state]
sm.mu.Unlock()
if tt.expectAction && action == nil {
t.Error("Expected action to be set, but it was nil")
}
if !tt.expectAction && action != nil {
t.Error("Expected action to be nil, but it was set")
}
})
}
}
func TestStateMachine_GetState(t *testing.T) {
tests := []struct {
name string
initialState State
transitions []Transition
events []Event
finalState State
}{
{
name: "get initial state",
initialState: StateIdle,
finalState: StateIdle,
},
{
name: "get state after transition",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
events: []Event{EventStart},
finalState: StateRunning,
},
{
name: "get state after multiple transitions",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
{From: StateRunning, Event: EventPause, To: StatePaused},
{From: StatePaused, Event: EventStart, To: StateRunning},
},
events: []Event{EventStart, EventPause, EventStart},
finalState: StateRunning,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(tt.initialState)
for _, transition := range tt.transitions {
sm.AddTransition(transition)
}
smImpl := sm.(*stateMachine)
for _, event := range tt.events {
if err := smImpl.handleEvent(event); err != nil {
t.Fatalf("Failed to handle event %v: %v", event, err)
}
}
if got := sm.GetState(); got != tt.finalState {
t.Errorf("GetState() = %v, want %v", got, tt.finalState)
}
})
}
}
func TestStateMachine_Start(t *testing.T) {
tests := []struct {
name string
initialState State
transitions []Transition
events []Event
cancelAfter time.Duration
expectError bool
expectedStates []State
}{
{
name: "start and cancel immediately",
initialState: StateIdle,
cancelAfter: 10 * time.Millisecond,
expectError: true, // context.Canceled
},
{
name: "process events then cancel",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
{From: StateRunning, Event: EventStop, To: StateStopped},
},
events: []Event{EventStart, EventStop},
cancelAfter: 100 * time.Millisecond,
expectError: true, // context.Canceled
expectedStates: []State{StateRunning, StateStopped},
},
{
name: "invalid transition error",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
events: []Event{EventPause}, // Invalid from StateIdle
cancelAfter: 50 * time.Millisecond,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(tt.initialState)
for _, transition := range tt.transitions {
sm.AddTransition(transition)
}
var states []State
var mu sync.Mutex
for _, state := range tt.expectedStates {
sm.SetAction(state, func(s State) {
mu.Lock()
states = append(states, s)
mu.Unlock()
})
}
ctx, cancel := context.WithCancel(context.Background())
errChan := make(chan error, 1)
go func() {
errChan <- sm.Start(ctx)
}()
time.Sleep(5 * time.Millisecond)
for _, event := range tt.events {
sm.SendEvent(event)
time.Sleep(5 * time.Millisecond)
}
time.Sleep(tt.cancelAfter)
cancel()
err := <-errChan
if tt.expectError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
time.Sleep(10 * time.Millisecond)
mu.Lock()
if len(states) != len(tt.expectedStates) {
t.Errorf("Expected %d state changes, got %d", len(tt.expectedStates), len(states))
}
for i, expectedState := range tt.expectedStates {
if i < len(states) && states[i] != expectedState {
t.Errorf("State change %d = %v, want %v", i, states[i], expectedState)
}
}
mu.Unlock()
})
}
}
func TestStateMachine_Reset(t *testing.T) {
tests := []struct {
name string
initialState State
resetState State
setupTransitions []Transition
eventsBeforeReset []Event
eventsAfterReset []Event
expectedState State
}{
{
name: "reset to same state",
initialState: StateIdle,
resetState: StateIdle,
expectedState: StateIdle,
},
{
name: "reset to different state",
initialState: StateIdle,
resetState: StateRunning,
expectedState: StateRunning,
},
{
name: "reset after state changes",
initialState: StateIdle,
resetState: StateIdle,
setupTransitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
eventsBeforeReset: []Event{EventStart},
expectedState: StateIdle,
},
{
name: "reset and send new events",
initialState: StateIdle,
resetState: StateIdle,
setupTransitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
{From: StateRunning, Event: EventStop, To: StateStopped},
},
eventsBeforeReset: []Event{EventStart},
eventsAfterReset: []Event{EventStart},
expectedState: StateIdle,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(tt.initialState)
smImpl := sm.(*stateMachine)
for _, transition := range tt.setupTransitions {
sm.AddTransition(transition)
}
for _, event := range tt.eventsBeforeReset {
if err := smImpl.handleEvent(event); err != nil {
// Ignore errors for this test
}
}
sm.Reset(tt.resetState)
if got := sm.GetState(); got != tt.expectedState {
t.Errorf("State after reset = %v, want %v", got, tt.expectedState)
}
for _, event := range tt.eventsAfterReset {
sm.SendEvent(event)
}
// For events after reset, we can't easily check the channel length
// due to the synchronization changes, so we just verify the reset worked
if len(tt.eventsAfterReset) > 0 {
time.Sleep(5 * time.Millisecond)
}
})
}
}
func TestStateMachine_Reset_WithRunningStateMachine(t *testing.T) {
sm := NewStateMachine(StateIdle)
sm.AddTransition(Transition{From: StateIdle, Event: EventStart, To: StateRunning})
sm.AddTransition(Transition{From: StateRunning, Event: EventStop, To: StateStopped})
var stateChanges []State
var mu sync.Mutex
sm.SetAction(StateRunning, func(s State) {
mu.Lock()
stateChanges = append(stateChanges, s)
mu.Unlock()
})
sm.SetAction(StateStopped, func(s State) {
mu.Lock()
stateChanges = append(stateChanges, s)
mu.Unlock()
})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := sm.Start(ctx); err != nil {
}
}()
// Give it time to start
time.Sleep(5 * time.Millisecond)
// Send an event
sm.SendEvent(EventStart)
time.Sleep(10 * time.Millisecond)
// Reset while running
sm.Reset(StateIdle)
// Verify state was reset
if got := sm.GetState(); got != StateIdle {
t.Errorf("State after reset = %v, want %v", got, StateIdle)
}
// Send another event after reset
sm.SendEvent(EventStart)
time.Sleep(10 * time.Millisecond)
mu.Lock()
changes := len(stateChanges)
mu.Unlock()
// Should have at least processed the first event
if changes < 1 {
t.Errorf("Expected at least 1 state change, got %d", changes)
}
}
func TestStateMachine_HandleEvent(t *testing.T) {
tests := []struct {
name string
initialState State
transitions []Transition
event Event
expectedState State
expectError bool
expectActionCall bool
}{
{
name: "valid transition",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
event: EventStart,
expectedState: StateRunning,
expectError: false,
expectActionCall: true,
},
{
name: "invalid transition",
initialState: StateIdle,
transitions: []Transition{
{From: StateRunning, Event: EventPause, To: StatePaused},
},
event: EventStart,
expectedState: StateIdle,
expectError: true,
expectActionCall: false,
},
{
name: "transition with no action",
initialState: StateIdle,
transitions: []Transition{
{From: StateIdle, Event: EventStart, To: StateRunning},
},
event: EventStart,
expectedState: StateRunning,
expectError: false,
expectActionCall: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := NewStateMachine(tt.initialState).(*stateMachine)
for _, transition := range tt.transitions {
sm.AddTransition(transition)
}
var actionCalled bool
var mu sync.Mutex
if tt.expectActionCall {
sm.SetAction(tt.expectedState, func(s State) {
mu.Lock()
actionCalled = true
mu.Unlock()
})
}
err := sm.handleEvent(tt.event)
if tt.expectError && err == nil {
t.Error("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if sm.GetState() != tt.expectedState {
t.Errorf("State after handleEvent = %v, want %v", sm.GetState(), tt.expectedState)
}
if tt.expectActionCall {
time.Sleep(10 * time.Millisecond)
mu.Lock()
called := actionCalled
mu.Unlock()
if !called {
t.Error("Expected action to be called but it wasn't")
}
}
})
}
}
func TestStateMachine_SendEvent_ThreadSafety(t *testing.T) {
sm := NewStateMachine(StateIdle)
sm.AddTransition(Transition{From: StateIdle, Event: EventStart, To: StateRunning})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := sm.Start(ctx); err != nil {
}
}()
time.Sleep(5 * time.Millisecond)
var wg sync.WaitGroup
numGoroutines := 10
eventsPerGoroutine := 100
// Send events concurrently
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < eventsPerGoroutine; j++ {
sm.SendEvent(EventStart)
}
}()
}
wg.Wait()
time.Sleep(10 * time.Millisecond)
// If we reach here without panicking, the test passes
}
+55 -594
View File
@@ -4,19 +4,16 @@ package cli
import (
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-sev-guest/tools/lib/report"
tpmAttest "github.com/google/go-tpm-tools/proto/attest"
@@ -25,25 +22,14 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
defaultMinimumTcb = 0
defaultMinimumLaunchTcb = 0
defaultMinimumGuestSvn = 0
defaultGuestPolicy = 0x0000000000030000
defaultMinimumBuild = 0
defaultCheckCrl = false
defaultTimeout = 2 * time.Minute
defaultMaxRetryDelay = 30 * time.Second
defaultRequireAuthor = false
defaultRequireIdBlock = false
defaultMinVersion = "0.0"
size8 = 8
size16 = 16
size32 = 32
size48 = 48
@@ -51,77 +37,19 @@ const (
attestationFilePath = "attestation.bin"
azureAttestResultFilePath = "azure_attest_result.json"
azureAttestTokenFilePath = "azure_attest_token.jwt"
vtpmFilePath = "../quote.dat"
attestationReportJson = "attestation.json"
sevProductNameMilan = "Milan"
sevProductNameGenoa = "Genoa"
FormatBinaryPB = "binarypb"
FormatTextProto = "textproto"
exampleJSONConfig = `
{
"rootOfTrust":{
"product":"test_product",
"cabundlePaths":[
"test_cabundlePaths"
],
"cabundles":[
"test_Cabundles"
],
"checkCrl":true,
"disallowNetwork":true
},
"policy":{
"minimumGuestSvn":1,
"policy":"1",
"familyId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"imageId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"vmpl":0,
"minimumTcb":"1",
"minimumLaunchTcb":"1",
"platformInfo":"1",
"requireAuthorKey":true,
"reportData":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"measurement":"8s78ewoX7Xkfy1qsgVnkZwLDotD768Nqt6qTL5wtQOxHsLczipKM6bhDmWiHLdP4",
"hostData":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportId":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportIdMa":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"chipId":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"minimumBuild":1,
"minimumVersion":"0.90",
"permitProvisionalFirmware":true,
"requireIdBlock":true,
"trustedAuthorKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedAuthorKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"product":{
"name":"SEV_PRODUCT_MILAN",
"stepping":1,
"machineStepping":1
}
}
}
`
SNP = "snp"
VTPM = "vtpm"
SNPvTPM = "snp-vtpm"
AzureToken = "azure-token"
CCNone = "none"
CCAzure = "azure"
CCGCP = "gcp"
TEE = "tee"
SNP = "snp"
VTPM = "vtpm"
SNPvTPM = "snp-vtpm"
AzureToken = "azure-token"
CCNone = "none"
CCAzure = "azure"
CCGCP = "gcp"
TDX = "tdx"
)
var (
mode string
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
cfgString string
timeout time.Duration
maxRetryDelay time.Duration
@@ -132,7 +60,6 @@ var (
trustedIdKeys []string
trustedIdKeyHashes []string
attestationFile string
tpmAttestationFile string
attestationRaw []byte
empty16 = [size16]byte{}
empty32 = [size32]byte{}
@@ -148,6 +75,8 @@ var (
getTextProtoAttestationReport bool
getAzureTokenJWT bool
cloud string
reportData []byte
checkCrl bool
)
var errEmptyFile = errors.New("input file is empty")
@@ -185,13 +114,14 @@ func (cli *CLI) NewAttestationCmd() *cobra.Command {
func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Retrieve attestation information from agent. The argument of the command must be the type of the report (snp or vtpm or snp-vtpm).",
ValidArgs: []cobra.Completion{SNP, VTPM, SNPvTPM, AzureToken},
Short: "Retrieve attestation information from agent. The argument of the command must be the type of the report (snp or vtpm or snp-vtpm or tdx).",
ValidArgs: []cobra.Completion{SNP, VTPM, SNPvTPM, AzureToken, TDX},
Example: fmt.Sprintf(`Based on attestation report type:
get %s --tee <512 bit hex value>
get %s --vtpm <256 bit hex value>
get %s --tee <512 bit hex value> --vtpm <256 bit hex value>
get %s --token <256 bit hex value>`, SNP, VTPM, SNPvTPM, AzureToken),
get %s --token <256 bit hex value>
get %s --tee <512 bit hex value>`, SNP, VTPM, SNPvTPM, AzureToken, TDX),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if cli.connectErr != nil {
@@ -219,6 +149,9 @@ func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
case AzureToken:
cmd.Println("Fetching Azure token")
attType = attestation.AzureToken
case TDX:
cmd.Println("Fetching TDX attestation report")
attType = attestation.TDX
}
if (attType == attestation.VTPM || attType == attestation.SNPvTPM) && len(nonce) == 0 {
@@ -352,7 +285,7 @@ func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
cmd.Flags().BoolVarP(&getAzureTokenJWT, "azurejwt", "t", false, "Get azure attestation token as jwt format")
cmd.Flags().BoolVarP(&getTextProtoAttestationReport, "reporttextproto", "r", false, "Get attestation report in textproto format")
cmd.Flags().BytesHexVar(&teeNonce, "tee", []byte{}, "Define the nonce for the SNP attestation report (must be used with attestation type snp and snp-vtpm)")
cmd.Flags().BytesHexVar(&teeNonce, "tee", []byte{}, "Define the nonce for the SNP and TDX attestation report (must be used with attestation type snp, snp-vtpm, and tdx)")
cmd.Flags().BytesHexVar(&nonce, "vtpm", []byte{}, "Define the nonce for the vTPM attestation report (must be used with attestation type vtpm and snp-vtpm)")
cmd.Flags().BytesHexVar(&tokenNonce, "token", []byte{}, "Define the nonce for the Azure attestation token (must be used with attestation type azure-token)")
@@ -387,12 +320,13 @@ func isFileJSON(filename string) bool {
func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate",
Short: fmt.Sprintf("Validate and verify attestation information. You can define the confidential computing cloud provider (%s, %s, %s; %s is the default) and can choose from 3 modes: %s, %s and %s. Default mode is %s.", CCNone, CCAzure, CCGCP, CCNone, SNP, VTPM, SNPvTPM, SNP),
Short: fmt.Sprintf("Validate and verify attestation information. You can define the confidential computing cloud provider (%s, %s, %s; %s is the default) and can choose from 4 modes: %s, %s, %s, and %s. Default mode is %s.", CCNone, CCAzure, CCGCP, CCNone, SNP, VTPM, SNPvTPM, TDX, SNP),
Example: `Based on mode:
validate <attestationreportfilepath> --report_data <reportdata> --product <product data> --platform <cc platform> //default
validate --mode snp <attestationreportfilepath> --report_data <reportdata> --product <product data>
validate --mode vtpm <attestationreportfilepath> --nonce <noncevalue> --format <formatvalue> --output <outputvalue>
validate --mode snp-vtpm <attestationreportfilepath> --report_data <reportdata> --product <product data> --nonce <noncevalue> --format <formatvalue> --output <outputvalue>
validate --mode tdx <attestationreportfilepath> --report_data <reportdata>
validate --cloud none --mode snp <attestationreportfilepath> --report_data <reportdata> --product <product data>
validate --cloud azure --mode vtpm <attestationreportfilepath> --nonce <noncevalue> --format <formatvalue> --output <outputvalue>
validate --cloud gcp --mode snp-vtpm <attestationreportfilepath> --report_data <reportdata> --product <product data> --nonce <noncevalue> --format <formatvalue> --output <outputvalue>`,
@@ -437,6 +371,10 @@ func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
if err := cmd.MarkFlagRequired("output"); err != nil {
return fmt.Errorf("failed to mark 'output' as required for %s mode: %v", VTPM, err)
}
case TDX:
if err := cmd.MarkFlagRequired("report_data"); err != nil {
return fmt.Errorf("failed to mark 'report_data' as required for %s mode: %v", TDX, err)
}
default:
return fmt.Errorf("unknown mode: %s", mode)
}
@@ -454,25 +392,38 @@ func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
defer closer.Close()
}
var provider attestation.Provider
var verifier attestation.Verifier
switch cloud {
case CCNone:
provider = vtpm.New(nil, false, 0, output)
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
case CCAzure:
provider = azure.New(output)
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
verifier = azure.NewVerifierWithPolicy(output, &policy)
case CCGCP:
provider = vtpm.New(nil, false, 0, output)
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
default:
provider = vtpm.New(nil, false, 0, output)
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
}
switch mode {
case SNP:
return sevsnpverify(cmd, provider, args)
cfg.Policy.ReportData = reportData
return sevsnpverify(cmd, verifier, args)
case SNPvTPM:
return vtpmSevSnpverify(args, provider)
cfg.Policy.ReportData = reportData
return vtpmSevSnpverify(args, verifier)
case VTPM:
return vtpmverify(args, provider)
cfg.Policy.ReportData = reportData
return vtpmverify(args, verifier)
case TDX:
if err := validateTDXFlags(); err != nil {
return fmt.Errorf("failed to verify TDX validation flags: %v ❌ ", err)
}
verifier = tdx.NewVerifierWithPolicy(cfgTDX)
return tdxVerify(args[0], verifier)
default:
return fmt.Errorf("unknown mode: %s", mode)
}
@@ -516,181 +467,21 @@ func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
"output file",
)
// SEV-SNP FLAGS
cmd.Flags().StringVar(
&cfgString,
"config",
"",
"Serialized json check.Config protobuf. This will overwrite individual flags. Unmarshalled as json. Example: "+exampleJSONConfig,
"Path to the serialized json check.Config protobuf file. This will overwrite individual flags. Unmarshalled as json. Example: "+exampleJSONConfig,
)
cmd.Flags().BytesHexVar(
&cfg.Policy.HostData,
"host_data",
empty32[:],
"The expected HOST_DATA field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ReportData,
&reportData,
"report_data",
empty64[:],
"The expected REPORT_DATA field as a hex string. Must encode 64 bytes. Must be set.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.FamilyId,
"family_id",
empty16[:],
"The expected FAMILY_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ImageId,
"image_id",
empty16[:],
"The expected IMAGE_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ReportId,
"report_id",
nil,
"The expected REPORT_ID field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ReportIdMa,
"report_id_ma",
defaultReportIdMa,
"The expected REPORT_ID_MA field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.Measurement,
"measurement",
nil,
"The expected MEASUREMENT field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ChipId,
"chip_id",
nil,
"The expected MEASUREMENT field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.MinimumTcb,
"minimum_tcb",
defaultMinimumTcb,
"The minimum acceptable value for CURRENT_TCB, COMMITTED_TCB, and REPORTED_TCB.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.MinimumLaunchTcb,
"minimum_lauch_tcb",
defaultMinimumLaunchTcb,
"The minimum acceptable value for LAUNCH_TCB.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.Policy,
"guest_policy",
defaultGuestPolicy,
"The most acceptable guest SnpPolicy.",
)
cmd.Flags().Uint32Var(
&cfg.Policy.MinimumGuestSvn,
"minimum_guest_svn",
defaultMinimumGuestSvn,
"The most acceptable GUEST_SVN.",
)
cmd.Flags().Uint32Var(
&cfg.Policy.MinimumBuild,
"minimum_build",
defaultMinimumBuild,
"The 8-bit minimum build number for AMD-SP firmware",
)
cmd.Flags().BoolVar(
&cfg.RootOfTrust.CheckCrl,
"check_crl",
defaultCheckCrl,
"Download and check the CRL for revoked certificates.",
)
cmd.Flags().DurationVar(
&timeout,
"timeout",
defaultTimeout,
"Duration to continue to retry failed HTTP requests.",
)
cmd.Flags().DurationVar(
&maxRetryDelay,
"max_retry_delay",
defaultMaxRetryDelay,
"Maximum Duration to wait between HTTP request retries.",
)
cmd.Flags().BoolVar(
&cfg.Policy.RequireAuthorKey,
"require_author_key",
defaultRequireAuthor,
"Require that AUTHOR_KEY_EN is 1.",
)
cmd.Flags().BoolVar(
&cfg.Policy.RequireIdBlock,
"require_id_block",
defaultRequireIdBlock,
"Require that the VM was launch with an ID_BLOCK signed by a trusted id key or author key",
)
cmd.Flags().StringVar(
&platformInfo,
"platform_info",
"",
"The maximum acceptable PLATFORM_INFO field bit-wise. May be empty or a 64-bit unsigned integer",
)
cmd.Flags().StringVar(
&cfg.Policy.MinimumVersion,
"minimum_version",
defaultMinVersion,
"Minimum AMD-SP firmware API version (major.minor). Each number must be 8-bit non-negative.",
)
cmd.Flags().StringArrayVar(
&trustedAuthorKeys,
"trusted_author_keys",
[]string{},
"Paths to x.509 certificates of trusted author keys",
)
cmd.Flags().StringArrayVar(
&trustedAuthorHashes,
"trusted_author_key_hashes",
[]string{},
"Hex-encoded SHA-384 hash values of trusted author keys in AMD public key format",
)
cmd.Flags().StringArrayVar(
&trustedIdKeys,
"trusted_id_keys",
[]string{},
"Paths to x.509 certificates of trusted author keys",
)
cmd.Flags().StringArrayVar(
&trustedIdKeyHashes,
"trusted_id_key_hashes",
[]string{},
"Hex-encoded SHA-384 hash values of trusted identity keys in AMD public key format",
)
cmd.Flags().StringVar(
&cfg.RootOfTrust.ProductLine,
"product",
"",
"The AMD product name for the chip that generated the attestation report.",
)
cmd.Flags().StringVar(
&stepping,
"stepping",
"",
"The machine stepping for the chip that generated the attestation report. Default unchecked.",
)
cmd.Flags().StringArrayVar(
&cfg.RootOfTrust.CabundlePaths,
"CA_bundles_paths",
[]string{},
"Paths to CA bundles for the AMD product. Must be in PEM format, ASK, then ARK certificates. If unset, uses embedded root certificates.",
)
cmd.Flags().StringArrayVar(
&cfg.RootOfTrust.Cabundles,
"CA_bundles",
[]string{},
"PEM format CA bundles for the AMD product. Combined with contents of cabundle_paths.",
)
cmd = addSEVSNPVerificationOptions(cmd)
cmd = addTDXVerificationOptions(cmd)
return cmd
}
@@ -733,130 +524,11 @@ func (cli *CLI) NewMeasureCmd(igvmBinaryPath string) *cobra.Command {
return igvmmeasureCmd
}
func sevsnpverify(cmd *cobra.Command, provider attestation.Provider, args []string) error {
cmd.Println("Checking attestation")
attestationFile = string(args[0])
if err := parseAttestationFile(); err != nil {
return fmt.Errorf("error parsing config: %v ❌ ", err)
}
// This format is the attestation report in AMD's specified ABI format, immediately
// followed by the certificate table bytes.
if len(attestationRaw) < abi.ReportSize {
return fmt.Errorf("attestation too small: got 0x%x bytes, need at least 0x%x bytes", len(attestationRaw), abi.ReportSize)
}
if err := parseAttestationConfig(); err != nil {
return err
}
// Used for verification of SNP attestation report
attestation.AttestationPolicy.Config = &cfg
if err := provider.VerifTeeAttestation(attestationRaw, cfg.Policy.ReportData); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
cmd.Println("Attestation validation and verification is successful!")
return nil
}
func parseAttestationConfig() error {
if err := parseConfig(); err != nil {
return fmt.Errorf("error parsing config: %v ❌ ", err)
}
if err := parseHashes(); err != nil {
return fmt.Errorf("error parsing hashes: %v ❌ ", err)
}
if err := parseTrustedKeys(); err != nil {
return fmt.Errorf("error parsing files: %v ❌ ", err)
}
if err := parseUints(); err != nil {
return fmt.Errorf("error parsing uints: %v ❌ ", err)
}
if err := validateInput(); err != nil {
return fmt.Errorf("error validating input: %v ❌ ", err)
}
return nil
}
func vtpmSevSnpverify(args []string, provider attestation.Provider) error {
attest, err := returnvTPMAttestation(args)
if err != nil {
return err
}
if err := parseAttestationConfig(); err != nil {
return err
}
// Used for verification of SNP attestation report
attestation.AttestationPolicy.Config = &cfg
if err := provider.VerifyAttestation(attest, cfg.Policy.ReportData, nonce); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
return nil
}
func vtpmverify(args []string, provider attestation.Provider) error {
attestation, err := returnvTPMAttestation(args)
if err != nil {
return err
}
if err := provider.VerifVTpmAttestation(attestation, nonce); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
return nil
}
func returnvTPMAttestation(args []string) ([]byte, error) {
tpmAttestationFile = string(args[0])
input, err := openInputFile()
if err != nil {
return nil, err
}
if closer, ok := input.(*os.File); ok {
defer closer.Close()
}
attestationBytes, err := io.ReadAll(input)
if err != nil {
return nil, err
}
attestation := &tpmAttest.Attestation{}
if format == FormatBinaryPB {
return attestationBytes, nil
} else if format == FormatTextProto {
unmarshalOptions := prototext.UnmarshalOptions{}
err = unmarshalOptions.Unmarshal(attestationBytes, attestation)
} else {
return nil, fmt.Errorf("format should be either binarypb or textproto")
}
if err != nil {
return nil, fmt.Errorf("fail to unmarshal attestation report: %v", err)
}
attestationBytes, err = proto.Marshal(attestation)
if err != nil {
return nil, fmt.Errorf("fail to marshal vTPM attestation report: %v", err)
}
return attestationBytes, nil
}
func openInputFile() (io.Reader, error) {
if tpmAttestationFile == "" {
if attestationFile == "" {
return nil, errEmptyFile
}
return os.Open(tpmAttestationFile)
return os.Open(attestationFile)
}
func createOutputFile() (io.Writer, error) {
@@ -866,217 +538,6 @@ func createOutputFile() (io.Writer, error) {
return os.Create(output)
}
// parseConfig decodes config passed as json for check.Config struct.
// example
/* {
"rootOfTrust":{
"product":"test_product",
"cabundlePaths":[
"test_cabundlePaths"
],
"cabundles":[
"test_Cabundles"
],
"checkCrl":true,
"disallowNetwork":true
},
"policy":{
"minimumGuestSvn":1,
"policy":"1",
"familyId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"imageId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"vmpl":0,
"minimumTcb":"1",
"minimumLaunchTcb":"1",
"platformInfo":"1",
"requireAuthorKey":true,
"reportData":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"measurement":"8s78ewoX7Xkfy1qsgVnkZwLDotD768Nqt6qTL5wtQOxHsLczipKM6bhDmWiHLdP4",
"hostData":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportId":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportIdMa":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"chipId":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"minimumBuild":1,
"minimumVersion":"0.90",
"permitProvisionalFirmware":true,
"requireIdBlock":true,
"trustedAuthorKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedAuthorKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"product":{
"name":"SEV_PRODUCT_MILAN",
"stepping":1,
"machineStepping":1
}
}
}*/
func parseConfig() error {
if cfgString == "" {
return nil
}
if err := protojson.Unmarshal([]byte(cfgString), &cfg); err != nil {
return err
}
// Populate fields that should not be nil
if cfg.RootOfTrust == nil {
cfg.RootOfTrust = &check.RootOfTrust{}
}
if cfg.Policy == nil {
cfg.Policy = &check.Policy{}
}
return nil
}
func parseHashes() error {
for _, hash := range trustedAuthorHashes {
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return err
}
cfg.Policy.TrustedAuthorKeyHashes = append(cfg.Policy.TrustedAuthorKeyHashes, hashBytes)
}
for _, hash := range trustedIdKeyHashes {
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return err
}
cfg.Policy.TrustedIdKeyHashes = append(cfg.Policy.TrustedIdKeyHashes, hashBytes)
}
return nil
}
func parseAttestationFile() error {
file, err := os.ReadFile(attestationFile)
if err != nil {
return err
}
attestationRaw = file
if isFileJSON(attestationFile) {
attestationRaw, err = attesationFromJSON(attestationRaw)
if err != nil {
return err
}
}
return nil
}
func parseTrustedKeys() error {
for _, path := range trustedAuthorKeys {
file, err := os.ReadFile(path)
if err != nil {
return err
}
cfg.Policy.TrustedAuthorKeys = append(cfg.Policy.TrustedAuthorKeys, file)
}
for _, path := range trustedIdKeys {
file, err := os.ReadFile(path)
if err != nil {
return err
}
cfg.Policy.TrustedIdKeys = append(cfg.Policy.TrustedIdKeys, file)
}
return nil
}
func parseUints() error {
if stepping != "" {
if base := getBase(stepping); base == 10 {
num, err := strconv.ParseUint(stepping, getBase(stepping), 8)
if err != nil {
return err
}
cfg.Policy.Product.MachineStepping = wrapperspb.UInt32(uint32(num))
} else {
num, err := strconv.ParseUint(stepping[2:], base, 8)
if err != nil {
return err
}
cfg.Policy.Product.MachineStepping = wrapperspb.UInt32(uint32(num))
}
}
if platformInfo != "" {
if base := getBase(platformInfo); base == 10 {
num, err := strconv.ParseUint(platformInfo, getBase(platformInfo), 8)
if err != nil {
return err
}
cfg.Policy.PlatformInfo = wrapperspb.UInt64(num)
} else {
num, err := strconv.ParseUint(platformInfo[2:], base, 8)
if err != nil {
return err
}
cfg.Policy.PlatformInfo = wrapperspb.UInt64(num)
}
}
return nil
}
func getBase(val string) int {
switch {
case strings.HasPrefix(val, "0x"):
return 16
case strings.HasPrefix(val, "0o"):
return 8
case strings.HasPrefix(val, "0b"):
return 2
default:
return 10
}
}
func validateInput() error {
if len(cfg.RootOfTrust.CabundlePaths) != 0 || len(cfg.RootOfTrust.Cabundles) != 0 && cfg.RootOfTrust.ProductLine == "" {
return fmt.Errorf("product name must be set if CA bundles are provided")
}
if err := validateFieldLength("report_data", cfg.Policy.ReportData, size64); err != nil {
return err
}
if err := validateFieldLength("host_data", cfg.Policy.HostData, size32); err != nil {
return err
}
if err := validateFieldLength("family_id", cfg.Policy.FamilyId, size16); err != nil {
return err
}
if err := validateFieldLength("image_id", cfg.Policy.ImageId, size16); err != nil {
return err
}
if err := validateFieldLength("report_id", cfg.Policy.ReportId, size32); err != nil {
return err
}
if err := validateFieldLength("report_id_ma", cfg.Policy.ReportIdMa, size32); err != nil {
return err
}
if err := validateFieldLength("measurement", cfg.Policy.Measurement, size48); err != nil {
return err
}
if err := validateFieldLength("chip_id", cfg.Policy.ChipId, size64); err != nil {
return err
}
for _, hash := range cfg.Policy.TrustedAuthorKeyHashes {
if err := validateFieldLength("trusted_author_key_hash", hash, size48); err != nil {
return err
}
}
for _, hash := range cfg.Policy.TrustedIdKeyHashes {
if err := validateFieldLength("trusted_id_key_hash", hash, size48); err != nil {
return err
}
}
return nil
}
func validateFieldLength(fieldName string, field []byte, expectedLength int) error {
if field != nil && len(field) != expectedLength {
return fmt.Errorf("%s length should be at least %d bytes long", fieldName, expectedLength)
+3 -2
View File
@@ -19,6 +19,7 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/gcp"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"google.golang.org/protobuf/proto"
)
@@ -291,7 +292,7 @@ func changeAttestationConfiguration(fileName, base64Data string, expectedLength
return errors.Wrap(errReadingAttestationPolicyFile, err)
}
if err = attestation.ReadAttestationPolicyFromByte(f, &ac); err != nil {
if err = vtpm.ReadPolicyFromByte(f, &ac); err != nil {
return errors.Wrap(errUnmarshalJSON, err)
}
@@ -308,7 +309,7 @@ func changeAttestationConfiguration(fileName, base64Data string, expectedLength
return errAttestationPolicyField
}
fileJson, err := json.MarshalIndent(&ac, "", " ")
fileJson, err := vtpm.ConvertPolicyToJSON(&ac)
if err != nil {
return errors.Wrap(errMarshalJSON, err)
}
+247 -1
View File
@@ -3,6 +3,7 @@
package cli
import (
"bytes"
"encoding/base64"
"encoding/json"
"os"
@@ -12,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
func TestChangeAttestationConfiguration(t *testing.T) {
@@ -88,7 +90,7 @@ func TestChangeAttestationConfiguration(t *testing.T) {
require.NoError(t, err)
ap := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
err = attestation.ReadAttestationPolicyFromByte(content, &ap)
err = vtpm.ReadPolicyFromByte(content, &ap)
require.NoError(t, err)
decodedData, _ := base64.StdEncoding.DecodeString(tt.base64Data)
@@ -130,3 +132,247 @@ func TestNewAddHostDataCmd(t *testing.T) {
assert.Equal(t, "hostdata <host-data> <attestation_policy.json>", cmd.Example)
assert.NotNil(t, cmd.Run)
}
func TestChangeAttestationConfigurationFileErrors(t *testing.T) {
t.Run("File Not Found", func(t *testing.T) {
err := changeAttestationConfiguration("nonexistent.json", base64.StdEncoding.EncodeToString(make([]byte, measurementLength)), measurementLength, measurementField)
assert.Error(t, err)
assert.Contains(t, err.Error(), "error while reading the attestation policy file")
})
t.Run("Invalid JSON Content", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "invalid.json")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("invalid json"), 0o644)
require.NoError(t, err)
err = changeAttestationConfiguration(tmpfile.Name(), base64.StdEncoding.EncodeToString(make([]byte, measurementLength)), measurementLength, measurementField)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to unmarshal json")
})
}
func TestNewGCPAttestationPolicy(t *testing.T) {
cli := &CLI{}
cmd := cli.NewGCPAttestationPolicy()
assert.Equal(t, "gcp", cmd.Use)
assert.Equal(t, "Get attestation policy for GCP CVM", cmd.Short)
assert.Equal(t, "gcp <bin_vtmp_attestation_report_file> <vcpu_count>", cmd.Example)
assert.NotNil(t, cmd.Run)
t.Run("File Not Found", func(t *testing.T) {
cmd.SetArgs([]string{"nonexistent.bin", "4"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error reading attestation report file")
assert.Contains(t, output, "❌")
})
t.Run("Invalid vCPU Count", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "attestation.bin")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("dummy content"), 0o644)
require.NoError(t, err)
cmd.SetArgs([]string{tmpfile.Name(), "invalid"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error converting vCPU count to integer")
assert.Contains(t, output, "❌")
})
t.Run("Invalid Attestation Data", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "attestation.bin")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("invalid protobuf data"), 0o644)
require.NoError(t, err)
cmd.SetArgs([]string{tmpfile.Name(), "4"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error unmarshaling attestation report")
assert.Contains(t, output, "❌")
})
}
func TestNewDownloadGCPOvmfFile(t *testing.T) {
cli := &CLI{}
cmd := cli.NewDownloadGCPOvmfFile()
assert.Equal(t, "download", cmd.Use)
assert.Equal(t, "Download GCP OVMF file", cmd.Short)
assert.Equal(t, "download <bin_vtmp_attestation_report_file>", cmd.Example)
assert.NotNil(t, cmd.Run)
t.Run("File Not Found", func(t *testing.T) {
cmd.SetArgs([]string{"nonexistent.bin"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error reading attestation report file")
assert.Contains(t, output, "❌")
})
t.Run("Invalid Attestation Data", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "attestation.bin")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("invalid protobuf data"), 0o644)
require.NoError(t, err)
cmd.SetArgs([]string{tmpfile.Name()})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error unmarshaling attestation report")
assert.Contains(t, output, "❌")
})
}
func TestNewAzureAttestationPolicy(t *testing.T) {
cli := &CLI{}
cmd := cli.NewAzureAttestationPolicy()
assert.Equal(t, "azure", cmd.Use)
assert.Equal(t, "Get attestation policy for Azure CVM", cmd.Short)
assert.Equal(t, "azure <azure_maa_token_file> <product_name>", cmd.Example)
assert.NotNil(t, cmd.Run)
flag := cmd.Flags().Lookup("policy")
assert.NotNil(t, flag)
assert.Equal(t, "Policy of the guest CVM", flag.Usage)
t.Run("File Not Found", func(t *testing.T) {
cmd.SetArgs([]string{"nonexistent.token", "test-product"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error reading attestation report file")
assert.Contains(t, output, "❌")
})
t.Run("Valid Token File", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "token.maa")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("dummy.token.content"), 0o644)
require.NoError(t, err)
defer os.Remove("attestation_policy.json")
cmd.SetArgs([]string{tmpfile.Name(), "test-product"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
assert.NoError(t, err)
})
t.Run("Custom Policy Flag", func(t *testing.T) {
tmpfile, err := os.CreateTemp("", "token.maa")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
err = os.WriteFile(tmpfile.Name(), []byte("dummy.token.content"), 0o644)
require.NoError(t, err)
cmd.SetArgs([]string{"--policy", "123456", tmpfile.Name(), "test-product"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
assert.NoError(t, err)
flag := cmd.Flags().Lookup("policy")
assert.NotNil(t, flag)
assert.Equal(t, "123456", flag.Value.String())
})
}
func TestCommandErrorHandling(t *testing.T) {
cli := &CLI{}
t.Run("Measurement Command Error", func(t *testing.T) {
cmd := cli.NewAddMeasurementCmd()
cmd.SetArgs([]string{"invalid-base64", "nonexistent.json"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error could not change measurement data")
assert.Contains(t, output, "❌")
})
t.Run("Host Data Command Error", func(t *testing.T) {
cmd := cli.NewAddHostDataCmd()
cmd.SetArgs([]string{"invalid-base64", "nonexistent.json"})
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, "Error could not change host data")
assert.Contains(t, output, "❌")
})
}
+597
View File
@@ -0,0 +1,597 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/hex"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
tpmAttest "github.com/google/go-tpm-tools/proto/attest"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/wrapperspb"
)
const (
defaultMinimumTcb = 0
defaultMinimumLaunchTcb = 0
defaultMinimumGuestSvn = 0
defaultGuestPolicy = 0x0000000000030000
defaultMinimumBuild = 0
defaultCheckCrl = false
defaultTimeout = 2 * time.Minute
defaultMaxRetryDelay = 30 * time.Second
defaultRequireAuthor = false
defaultRequireIdBlock = false
defaultMinVersion = "0.0"
vtpmFilePath = "../quote.dat"
attestationReportJson = "attestation.json"
sevSnpProductMilan = "Milan"
sevSnpProductGenoa = "Genoa"
FormatBinaryPB = "binarypb"
FormatTextProto = "textproto"
exampleJSONConfig = `
{
"rootOfTrust":{
"product":"test_product",
"cabundlePaths":[
"test_cabundlePaths"
],
"cabundles":[
"test_Cabundles"
],
"checkCrl":true,
"disallowNetwork":true
},
"policy":{
"minimumGuestSvn":1,
"policy":"1",
"familyId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"imageId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"vmpl":0,
"minimumTcb":"1",
"minimumLaunchTcb":"1",
"platformInfo":"1",
"requireAuthorKey":true,
"reportData":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"measurement":"8s78ewoX7Xkfy1qsgVnkZwLDotD768Nqt6qTL5wtQOxHsLczipKM6bhDmWiHLdP4",
"hostData":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportId":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportIdMa":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"chipId":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"minimumBuild":1,
"minimumVersion":"0.90",
"permitProvisionalFirmware":true,
"requireIdBlock":true,
"trustedAuthorKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedAuthorKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"product":{
"name":1,
"stepping":1,
"machineStepping":1
}
}
}
`
)
var cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
func addSEVSNPVerificationOptions(cmd *cobra.Command) *cobra.Command {
cmd.Flags().BytesHexVar(
&cfg.Policy.HostData,
"host_data",
empty32[:],
"The expected HOST_DATA field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.FamilyId,
"family_id",
empty16[:],
"The expected FAMILY_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ImageId,
"image_id",
empty16[:],
"The expected IMAGE_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ReportId,
"report_id",
nil,
"The expected REPORT_ID field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ReportIdMa,
"report_id_ma",
defaultReportIdMa,
"The expected REPORT_ID_MA field as a hex string. Must encode 32 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.Measurement,
"measurement",
nil,
"The expected MEASUREMENT field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfg.Policy.ChipId,
"chip_id",
nil,
"The expected MEASUREMENT field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.MinimumTcb,
"minimum_tcb",
defaultMinimumTcb,
"The minimum acceptable value for CURRENT_TCB, COMMITTED_TCB, and REPORTED_TCB.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.MinimumLaunchTcb,
"minimum_lauch_tcb",
defaultMinimumLaunchTcb,
"The minimum acceptable value for LAUNCH_TCB.",
)
cmd.Flags().Uint64Var(
&cfg.Policy.Policy,
"guest_policy",
defaultGuestPolicy,
"The most acceptable guest SnpPolicy.",
)
cmd.Flags().Uint32Var(
&cfg.Policy.MinimumGuestSvn,
"minimum_guest_svn",
defaultMinimumGuestSvn,
"The most acceptable GUEST_SVN.",
)
cmd.Flags().Uint32Var(
&cfg.Policy.MinimumBuild,
"minimum_build",
defaultMinimumBuild,
"The 8-bit minimum build number for AMD-SP firmware",
)
cmd.Flags().BoolVar(
&checkCrl,
"check_crl",
defaultCheckCrl,
"Download and check the CRL for revoked certificates.",
)
cmd.Flags().DurationVar(
&timeout,
"timeout",
defaultTimeout,
"Duration to continue to retry failed HTTP requests.",
)
cmd.Flags().DurationVar(
&maxRetryDelay,
"max_retry_delay",
defaultMaxRetryDelay,
"Maximum Duration to wait between HTTP request retries.",
)
cmd.Flags().BoolVar(
&cfg.Policy.RequireAuthorKey,
"require_author_key",
defaultRequireAuthor,
"Require that AUTHOR_KEY_EN is 1.",
)
cmd.Flags().BoolVar(
&cfg.Policy.RequireIdBlock,
"require_id_block",
defaultRequireIdBlock,
"Require that the VM was launch with an ID_BLOCK signed by a trusted id key or author key",
)
cmd.Flags().StringVar(
&platformInfo,
"platform_info",
"",
"The maximum acceptable PLATFORM_INFO field bit-wise. May be empty or a 64-bit unsigned integer",
)
cmd.Flags().StringVar(
&cfg.Policy.MinimumVersion,
"minimum_version",
defaultMinVersion,
"Minimum AMD-SP firmware API version (major.minor). Each number must be 8-bit non-negative.",
)
cmd.Flags().StringArrayVar(
&trustedAuthorKeys,
"trusted_author_keys",
[]string{},
"Paths to x.509 certificates of trusted author keys",
)
cmd.Flags().StringArrayVar(
&trustedAuthorHashes,
"trusted_author_key_hashes",
[]string{},
"Hex-encoded SHA-384 hash values of trusted author keys in AMD public key format",
)
cmd.Flags().StringArrayVar(
&trustedIdKeys,
"trusted_id_keys",
[]string{},
"Paths to x.509 certificates of trusted author keys",
)
cmd.Flags().StringArrayVar(
&trustedIdKeyHashes,
"trusted_id_key_hashes",
[]string{},
"Hex-encoded SHA-384 hash values of trusted identity keys in AMD public key format",
)
cmd.Flags().StringVar(
&cfg.RootOfTrust.ProductLine,
"product",
"",
"The AMD product name for the chip that generated the attestation report.",
)
cmd.Flags().StringVar(
&stepping,
"stepping",
"",
"The machine stepping for the chip that generated the attestation report. Default unchecked.",
)
cmd.Flags().StringArrayVar(
&cfg.RootOfTrust.CabundlePaths,
"CA_bundles_paths",
[]string{},
"Paths to CA bundles for the AMD product. Must be in PEM format, ASK, then ARK certificates. If unset, uses embedded root certificates.",
)
cmd.Flags().StringArrayVar(
&cfg.RootOfTrust.Cabundles,
"CA_bundles",
[]string{},
"PEM format CA bundles for the AMD product. Combined with contents of cabundle_paths.",
)
return cmd
}
func validateInput() error {
if len(cfg.RootOfTrust.CabundlePaths) != 0 || len(cfg.RootOfTrust.Cabundles) != 0 && cfg.RootOfTrust.ProductLine == "" {
return fmt.Errorf("product name must be set if CA bundles are provided")
}
if err := validateFieldLength("report_data", cfg.Policy.ReportData, size64); err != nil {
return err
}
if err := validateFieldLength("host_data", cfg.Policy.HostData, size32); err != nil {
return err
}
if err := validateFieldLength("family_id", cfg.Policy.FamilyId, size16); err != nil {
return err
}
if err := validateFieldLength("image_id", cfg.Policy.ImageId, size16); err != nil {
return err
}
if err := validateFieldLength("report_id", cfg.Policy.ReportId, size32); err != nil {
return err
}
if err := validateFieldLength("report_id_ma", cfg.Policy.ReportIdMa, size32); err != nil {
return err
}
if err := validateFieldLength("measurement", cfg.Policy.Measurement, size48); err != nil {
return err
}
if err := validateFieldLength("chip_id", cfg.Policy.ChipId, size64); err != nil {
return err
}
for _, hash := range cfg.Policy.TrustedAuthorKeyHashes {
if err := validateFieldLength("trusted_author_key_hash", hash, size48); err != nil {
return err
}
}
for _, hash := range cfg.Policy.TrustedIdKeyHashes {
if err := validateFieldLength("trusted_id_key_hash", hash, size48); err != nil {
return err
}
}
return nil
}
func parseTrustedKeys() error {
for _, path := range trustedAuthorKeys {
file, err := os.ReadFile(path)
if err != nil {
return err
}
cfg.Policy.TrustedAuthorKeys = append(cfg.Policy.TrustedAuthorKeys, file)
}
for _, path := range trustedIdKeys {
file, err := os.ReadFile(path)
if err != nil {
return err
}
cfg.Policy.TrustedIdKeys = append(cfg.Policy.TrustedIdKeys, file)
}
return nil
}
func parseUints() error {
if stepping != "" {
if base := getBase(stepping); base == 10 {
num, err := strconv.ParseUint(stepping, getBase(stepping), 8)
if err != nil {
return err
}
cfg.Policy.Product.MachineStepping = wrapperspb.UInt32(uint32(num))
} else {
num, err := strconv.ParseUint(stepping[2:], base, 8)
if err != nil {
return err
}
cfg.Policy.Product.MachineStepping = wrapperspb.UInt32(uint32(num))
}
}
if platformInfo != "" {
if base := getBase(platformInfo); base == 10 {
num, err := strconv.ParseUint(platformInfo, getBase(platformInfo), 8)
if err != nil {
return err
}
cfg.Policy.PlatformInfo = wrapperspb.UInt64(num)
} else {
num, err := strconv.ParseUint(platformInfo[2:], base, 8)
if err != nil {
return err
}
cfg.Policy.PlatformInfo = wrapperspb.UInt64(num)
}
}
return nil
}
func getBase(val string) int {
switch {
case strings.HasPrefix(val, "0x"):
return 16
case strings.HasPrefix(val, "0o"):
return 8
case strings.HasPrefix(val, "0b"):
return 2
default:
return 10
}
}
// parseConfig decodes config passed as json for check.Config struct.
// example
/* {
"rootOfTrust":{
"product":"test_product",
"cabundlePaths":[
"test_cabundlePaths"
],
"cabundles":[
"test_Cabundles"
],
"checkCrl":true,
"disallowNetwork":true
},
"policy":{
"minimumGuestSvn":1,
"policy":"1",
"familyId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"imageId":"AQIDBAUGBwgJCgsMDQ4PEA==",
"vmpl":0,
"minimumTcb":"1",
"minimumLaunchTcb":"1",
"platformInfo":"1",
"requireAuthorKey":true,
"reportData":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"measurement":"8s78ewoX7Xkfy1qsgVnkZwLDotD768Nqt6qTL5wtQOxHsLczipKM6bhDmWiHLdP4",
"hostData":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportId":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"reportIdMa":"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw=",
"chipId":"J+60aXs8btm8VcGgaJYURGeNCu0FIyWMFXQ7ZUlJDC0FJGJizJsOzDIXgQ75UtPC+Zqe0A3dvnnf5VEeQ61RTg==",
"minimumBuild":1,
"minimumVersion":"0.90",
"permitProvisionalFirmware":true,
"requireIdBlock":true,
"trustedAuthorKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedAuthorKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeys":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"trustedIdKeyHashes":[
"GSvLKpfu59Y9QOF6vhq0vQsOIvb4+5O/UOHLGLBTkdw="
],
"product":{
"name":"1",
"stepping":1,
"machineStepping":1
}
}
}*/
func parseConfig() error {
if cfgString == "" {
return nil
}
policyByte, err := os.ReadFile(cfgString)
if err != nil {
return err
}
if err := protojson.Unmarshal(policyByte, &cfg); err != nil {
return err
}
// Populate fields that should not be nil
if cfg.RootOfTrust == nil {
cfg.RootOfTrust = &check.RootOfTrust{}
}
if cfg.Policy == nil {
cfg.Policy = &check.Policy{}
}
return nil
}
func parseHashes() error {
for _, hash := range trustedAuthorHashes {
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return err
}
cfg.Policy.TrustedAuthorKeyHashes = append(cfg.Policy.TrustedAuthorKeyHashes, hashBytes)
}
for _, hash := range trustedIdKeyHashes {
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return err
}
cfg.Policy.TrustedIdKeyHashes = append(cfg.Policy.TrustedIdKeyHashes, hashBytes)
}
return nil
}
func parseAttestationFile() error {
file, err := os.ReadFile(attestationFile)
if err != nil {
return err
}
attestationRaw = file
if isFileJSON(attestationFile) {
attestationRaw, err = attesationFromJSON(attestationRaw)
if err != nil {
return err
}
}
return nil
}
func sevsnpverify(cmd *cobra.Command, verifier attestation.Verifier, args []string) error {
cmd.Println("Checking attestation")
attestationFile = string(args[0])
if err := parseAttestationFile(); err != nil {
return fmt.Errorf("error parsing config: %v ❌ ", err)
}
// This format is the attestation report in AMD's specified ABI format, immediately
// followed by the certificate table bytes.
if len(attestationRaw) < abi.ReportSize {
return fmt.Errorf("attestation too small: got 0x%x bytes, need at least 0x%x bytes", len(attestationRaw), abi.ReportSize)
}
if err := parseAttestationConfig(); err != nil {
return err
}
if err := verifier.VerifTeeAttestation(attestationRaw, cfg.Policy.ReportData); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
cmd.Println("Attestation validation and verification is successful!")
return nil
}
func parseAttestationConfig() error {
if err := parseConfig(); err != nil {
return fmt.Errorf("error parsing config: %v ❌ ", err)
}
if err := parseHashes(); err != nil {
return fmt.Errorf("error parsing hashes: %v ❌ ", err)
}
if err := parseTrustedKeys(); err != nil {
return fmt.Errorf("error parsing files: %v ❌ ", err)
}
if err := parseUints(); err != nil {
return fmt.Errorf("error parsing uints: %v ❌ ", err)
}
if err := validateInput(); err != nil {
return fmt.Errorf("error validating input: %v ❌ ", err)
}
return nil
}
func vtpmSevSnpverify(args []string, verifier attestation.Verifier) error {
attest, err := returnvTPMAttestation(args)
if err != nil {
return err
}
if err := parseAttestationConfig(); err != nil {
return err
}
if err := verifier.VerifyAttestation(attest, cfg.Policy.ReportData, nonce); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
return nil
}
func vtpmverify(args []string, verifier attestation.Verifier) error {
attestation, err := returnvTPMAttestation(args)
if err != nil {
return err
}
if err := verifier.VerifVTpmAttestation(attestation, nonce); err != nil {
return fmt.Errorf("attestation validation and verification failed with error: %v ❌ ", err)
}
return nil
}
func returnvTPMAttestation(args []string) ([]byte, error) {
attestationFile = string(args[0])
input, err := openInputFile()
if err != nil {
return nil, err
}
if closer, ok := input.(*os.File); ok {
defer closer.Close()
}
attestationBytes, err := io.ReadAll(input)
if err != nil {
return nil, err
}
attestation := &tpmAttest.Attestation{}
if format == FormatBinaryPB {
return attestationBytes, nil
} else if format == FormatTextProto {
unmarshalOptions := prototext.UnmarshalOptions{}
err = unmarshalOptions.Unmarshal(attestationBytes, attestation)
} else {
return nil, fmt.Errorf("format should be either binarypb or textproto")
}
if err != nil {
return nil, fmt.Errorf("fail to unmarshal attestation report: %v", err)
}
attestationBytes, err = proto.Marshal(attestation)
if err != nil {
return nil, fmt.Errorf("fail to marshal vTPM attestation report: %v", err)
}
return attestationBytes, nil
}
+870
View File
@@ -0,0 +1,870 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
tpmAttest "github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm-tools/proto/tpm"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation/mocks"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
func TestAddSEVSNPVerificationOptions(t *testing.T) {
cmd := &cobra.Command{
Use: "test",
}
result := addSEVSNPVerificationOptions(cmd)
assert.Equal(t, cmd, result)
// Check that important flags are added
flags := []string{
"host_data",
"family_id",
"image_id",
"report_id",
"report_id_ma",
"measurement",
"chip_id",
"minimum_tcb",
"minimum_lauch_tcb",
"guest_policy",
"minimum_guest_svn",
"minimum_build",
"check_crl",
"timeout",
"max_retry_delay",
"require_author_key",
"require_id_block",
"platform_info",
"minimum_version",
"trusted_author_keys",
"trusted_author_key_hashes",
"trusted_id_keys",
"trusted_id_key_hashes",
"product",
"stepping",
"CA_bundles_paths",
"CA_bundles",
}
for _, flagName := range flags {
flag := cmd.Flags().Lookup(flagName)
assert.NotNil(t, flag, "Flag %s should exist", flagName)
}
}
func TestValidateInput(t *testing.T) {
tests := []struct {
name string
setupCfg func()
expectErr bool
errMsg string
}{
{
name: "valid empty config",
setupCfg: func() {
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
},
expectErr: false,
},
{
name: "CA bundles without product name",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{},
RootOfTrust: &check.RootOfTrust{
CabundlePaths: []string{"test.pem"},
ProductLine: "",
},
}
},
expectErr: true,
errMsg: "product name must be set if CA bundles are provided",
},
{
name: "invalid report_data length",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
ReportData: []byte("invalid"),
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "report_data",
},
{
name: "invalid host_data length",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
HostData: []byte("invalid"),
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "host_data",
},
{
name: "invalid family_id length",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
FamilyId: []byte("invalid"),
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "family_id",
},
{
name: "invalid image_id length",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
ImageId: []byte("invalid"),
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "image_id",
},
{
name: "invalid trusted author key hash",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
TrustedAuthorKeyHashes: [][]byte{[]byte("invalid")},
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "trusted_author_key_hash",
},
{
name: "invalid trusted id key hash",
setupCfg: func() {
cfg = check.Config{
Policy: &check.Policy{
TrustedIdKeyHashes: [][]byte{[]byte("invalid")},
},
RootOfTrust: &check.RootOfTrust{},
}
},
expectErr: true,
errMsg: "trusted_id_key_hash",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupCfg()
err := validateInput()
if tt.expectErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestParseTrustedKeys(t *testing.T) {
tempDir := t.TempDir()
authorKeyFile := filepath.Join(tempDir, "author.pem")
idKeyFile := filepath.Join(tempDir, "id.pem")
nonExistentFile := filepath.Join(tempDir, "nonexistent.pem")
authorKeyContent := "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAOI..."
idKeyContent := "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAOI..."
require.NoError(t, os.WriteFile(authorKeyFile, []byte(authorKeyContent), 0o644))
require.NoError(t, os.WriteFile(idKeyFile, []byte(idKeyContent), 0o644))
tests := []struct {
name string
trustedAuthorKeys []string
trustedIdKeys []string
expectErr bool
}{
{
name: "valid files",
trustedAuthorKeys: []string{authorKeyFile},
trustedIdKeys: []string{idKeyFile},
expectErr: false,
},
{
name: "nonexistent author key file",
trustedAuthorKeys: []string{nonExistentFile},
trustedIdKeys: []string{},
expectErr: true,
},
{
name: "nonexistent id key file",
trustedAuthorKeys: []string{},
trustedIdKeys: []string{nonExistentFile},
expectErr: true,
},
{
name: "empty file lists",
trustedAuthorKeys: []string{},
trustedIdKeys: []string{},
expectErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
trustedAuthorKeys = tt.trustedAuthorKeys
trustedIdKeys = tt.trustedIdKeys
err := parseTrustedKeys()
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if len(tt.trustedAuthorKeys) > 0 {
assert.Len(t, cfg.Policy.TrustedAuthorKeys, len(tt.trustedAuthorKeys))
assert.Equal(t, []byte(authorKeyContent), cfg.Policy.TrustedAuthorKeys[0])
}
if len(tt.trustedIdKeys) > 0 {
assert.Len(t, cfg.Policy.TrustedIdKeys, len(tt.trustedIdKeys))
assert.Equal(t, []byte(idKeyContent), cfg.Policy.TrustedIdKeys[0])
}
}
})
}
}
func TestParseUints(t *testing.T) {
tests := []struct {
name string
stepping string
platformInfo string
expectErr bool
expectedStep *uint32
expectedPlatform *uint64
}{
{
name: "empty values",
stepping: "",
platformInfo: "",
expectErr: false,
},
{
name: "decimal values",
stepping: "5",
platformInfo: "10",
expectErr: false,
expectedStep: uint32Ptr(5),
expectedPlatform: uint64Ptr(10),
},
{
name: "hex values",
stepping: "0x5",
platformInfo: "0xa",
expectErr: false,
expectedStep: uint32Ptr(5),
expectedPlatform: uint64Ptr(10),
},
{
name: "octal values",
stepping: "0o7",
platformInfo: "0o12",
expectErr: false,
expectedStep: uint32Ptr(7),
expectedPlatform: uint64Ptr(10),
},
{
name: "binary values",
stepping: "0b101",
platformInfo: "0b1010",
expectErr: false,
expectedStep: uint32Ptr(5),
expectedPlatform: uint64Ptr(10),
},
{
name: "invalid stepping",
stepping: "invalid",
platformInfo: "",
expectErr: true,
},
{
name: "invalid platform info",
stepping: "",
platformInfo: "invalid",
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg = check.Config{Policy: &check.Policy{Product: &sevsnp.SevProduct{}}, RootOfTrust: &check.RootOfTrust{}}
stepping = tt.stepping
platformInfo = tt.platformInfo
err := parseUints()
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.expectedStep != nil {
assert.Equal(t, *tt.expectedStep, cfg.Policy.Product.MachineStepping.Value)
}
if tt.expectedPlatform != nil {
assert.Equal(t, *tt.expectedPlatform, cfg.Policy.PlatformInfo.Value)
}
}
})
}
}
func TestGetBase(t *testing.T) {
tests := []struct {
input string
expected int
}{
{"0x10", 16},
{"0o10", 8},
{"0b10", 2},
{"10", 10},
{"", 10},
{"abc", 10},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := getBase(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseConfig(t *testing.T) {
tempDir := t.TempDir()
validConfig := map[string]interface{}{
"rootOfTrust": map[string]interface{}{
"product": "test_product",
"cabundlePaths": []string{"test_path"},
"cabundles": []string{"test_bundle"},
"checkCrl": true,
"disallowNetwork": true,
},
"policy": map[string]interface{}{
"minimumGuestSvn": 1,
"policy": "1",
"minimumBuild": 1,
"minimumVersion": "0.90",
"requireAuthorKey": true,
"requireIdBlock": true,
},
}
tests := []struct {
name string
setupConfig func() string
expectErr bool
}{
{
name: "empty config string",
setupConfig: func() string {
return ""
},
expectErr: false,
},
{
name: "valid config file",
setupConfig: func() string {
configFile := filepath.Join(tempDir, "valid_config.json")
configBytes, err := json.Marshal(validConfig)
assert.NoError(t, err)
if err := os.WriteFile(configFile, configBytes, 0o644); err != nil {
t.Errorf("failed to write config file: %v", err)
}
return configFile
},
expectErr: false,
},
{
name: "nonexistent config file",
setupConfig: func() string {
return filepath.Join(tempDir, "nonexistent.json")
},
expectErr: true,
},
{
name: "invalid JSON config",
setupConfig: func() string {
configFile := filepath.Join(tempDir, "invalid_config.json")
if err := os.WriteFile(configFile, []byte("invalid json"), 0o644); err != nil {
t.Errorf("failed to write invalid config file: %v", err)
}
return configFile
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
cfgString = tt.setupConfig()
err := parseConfig()
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, cfg.Policy)
assert.NotNil(t, cfg.RootOfTrust)
}
})
}
}
func TestParseHashes(t *testing.T) {
tests := []struct {
name string
trustedAuthorHashes []string
trustedIdKeyHashes []string
expectErr bool
}{
{
name: "valid hashes",
trustedAuthorHashes: []string{"deadbeef", "cafebabe"},
trustedIdKeyHashes: []string{"12345678", "87654321"},
expectErr: false,
},
{
name: "empty hashes",
trustedAuthorHashes: []string{},
trustedIdKeyHashes: []string{},
expectErr: false,
},
{
name: "invalid author hash",
trustedAuthorHashes: []string{"invalid_hex"},
trustedIdKeyHashes: []string{},
expectErr: true,
},
{
name: "invalid id key hash",
trustedAuthorHashes: []string{},
trustedIdKeyHashes: []string{"invalid_hex"},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
trustedAuthorHashes = tt.trustedAuthorHashes
trustedIdKeyHashes = tt.trustedIdKeyHashes
err := parseHashes()
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Len(t, cfg.Policy.TrustedAuthorKeyHashes, len(tt.trustedAuthorHashes))
assert.Len(t, cfg.Policy.TrustedIdKeyHashes, len(tt.trustedIdKeyHashes))
for i, hash := range tt.trustedAuthorHashes {
expected, _ := hex.DecodeString(hash)
assert.Equal(t, expected, cfg.Policy.TrustedAuthorKeyHashes[i])
}
for i, hash := range tt.trustedIdKeyHashes {
expected, _ := hex.DecodeString(hash)
assert.Equal(t, expected, cfg.Policy.TrustedIdKeyHashes[i])
}
}
})
}
}
func TestParseAttestationFile(t *testing.T) {
tempDir := t.TempDir()
binaryFile := filepath.Join(tempDir, "attestation.bin")
jsonFile := filepath.Join(tempDir, "attestation.json")
binaryData := make([]byte, 1024)
for i := range binaryData {
binaryData[i] = byte(i % 256)
}
jsonData := &sevsnp.Attestation{
Report: &sevsnp.Report{
FamilyId: make([]byte, 16),
ImageId: make([]byte, 16),
ReportData: make([]byte, 64),
Measurement: make([]byte, 48),
HostData: make([]byte, 32),
IdKeyDigest: make([]byte, 48),
AuthorKeyDigest: make([]byte, 48),
ReportId: make([]byte, 32),
ReportIdMa: make([]byte, 32),
ChipId: make([]byte, 64),
Signature: make([]byte, 512),
},
}
jsonBytes, err := json.Marshal(jsonData)
require.NoError(t, err)
require.NoError(t, os.WriteFile(binaryFile, binaryData, 0o644))
require.NoError(t, os.WriteFile(jsonFile, jsonBytes, 0o644))
tests := []struct {
name string
attestationFile string
expectErr bool
}{
{
name: "valid binary file",
attestationFile: binaryFile,
expectErr: false,
},
{
name: "valid JSON file",
attestationFile: jsonFile,
expectErr: false,
},
{
name: "nonexistent file",
attestationFile: filepath.Join(tempDir, "nonexistent.bin"),
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
attestationFile = tt.attestationFile
err := parseAttestationFile()
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, attestationRaw)
assert.NotEmpty(t, attestationRaw)
}
})
}
}
func TestSevsnpverify(t *testing.T) {
trustedAuthorHashes = []string{}
trustedIdKeyHashes = []string{}
stepping = ""
platformInfo = ""
tempDir := t.TempDir()
cfg = check.Config{Policy: &check.Policy{Product: &sevsnp.SevProduct{}}, RootOfTrust: &check.RootOfTrust{}}
attestationFile := filepath.Join(tempDir, "attestation.bin")
attestationData := make([]byte, abi.ReportSize+100)
for i := range attestationData {
attestationData[i] = byte(i % 256)
}
require.NoError(t, os.WriteFile(attestationFile, attestationData, 0o644))
tests := []struct {
name string
args []string
setupMock func(*mocks.Verifier)
expectErr bool
expectedMsg string
}{
{
name: "successful verification",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifTeeAttestation", mock.Anything, mock.Anything).Return(nil)
},
expectErr: false,
expectedMsg: "Attestation validation and verification is successful!",
},
{
name: "verification failure",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifTeeAttestation", mock.Anything, mock.Anything).Return(fmt.Errorf("verification failed"))
},
expectErr: true,
expectedMsg: "attestation validation and verification failed",
},
{
name: "nonexistent file",
args: []string{filepath.Join(tempDir, "nonexistent.bin")},
setupMock: func(m *mocks.Verifier) {},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfgString = ""
mockVerifier := new(mocks.Verifier)
tt.setupMock(mockVerifier)
var output bytes.Buffer
cmd := &cobra.Command{}
cmd.SetOut(&output)
err := sevsnpverify(cmd, mockVerifier, tt.args)
fmt.Println("error1", err)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
if tt.expectedMsg != "" {
assert.Contains(t, output.String(), tt.expectedMsg)
}
}
mockVerifier.AssertExpectations(t)
})
}
}
func TestReturnvTPMAttestation(t *testing.T) {
tempDir := t.TempDir()
attestation := &tpmAttest.Attestation{
Quotes: []*tpm.Quote{
{
Quote: []byte("test quote"),
RawSig: []byte("test signature"),
},
},
}
binaryData, err := proto.Marshal(attestation)
require.NoError(t, err)
binaryFile := filepath.Join(tempDir, "attestation.pb")
require.NoError(t, os.WriteFile(binaryFile, binaryData, 0o644))
textData, err := prototext.Marshal(attestation)
require.NoError(t, err)
textFile := filepath.Join(tempDir, "attestation.txtpb")
require.NoError(t, os.WriteFile(textFile, textData, 0o644))
tests := []struct {
name string
args []string
format string
expectErr bool
}{
{
name: "binary protobuf format",
args: []string{binaryFile},
format: FormatBinaryPB,
expectErr: false,
},
{
name: "text protobuf format",
args: []string{textFile},
format: FormatTextProto,
expectErr: false,
},
{
name: "invalid format",
args: []string{binaryFile},
format: "invalid",
expectErr: true,
},
{
name: "nonexistent file",
args: []string{filepath.Join(tempDir, "nonexistent.pb")},
format: FormatBinaryPB,
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
format = tt.format
result, err := returnvTPMAttestation(tt.args)
if tt.expectErr {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result)
}
})
}
}
func TestVtpmSevSnpverify(t *testing.T) {
stepping = ""
platformInfo = ""
trustedAuthorHashes = []string{}
trustedIdKeyHashes = []string{}
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
tempDir := t.TempDir()
attestation := &tpmAttest.Attestation{
Quotes: []*tpm.Quote{
{
Quote: []byte("test quote"),
RawSig: []byte("test signature"),
},
},
}
binaryData, err := proto.Marshal(attestation)
require.NoError(t, err)
attestationFile := filepath.Join(tempDir, "vtpm_attestation.pb")
require.NoError(t, os.WriteFile(attestationFile, binaryData, 0o644))
tests := []struct {
name string
args []string
setupMock func(*mocks.Verifier)
expectErr bool
}{
{
name: "successful verification",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifyAttestation", mock.Anything, mock.Anything, mock.Anything).Return(nil)
},
expectErr: false,
},
{
name: "verification failure",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifyAttestation", mock.Anything, mock.Anything, mock.Anything).Return(fmt.Errorf("verification failed"))
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg = check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}
cfgString = ""
format = FormatBinaryPB
mockVerifier := new(mocks.Verifier)
tt.setupMock(mockVerifier)
err := vtpmSevSnpverify(tt.args, mockVerifier)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockVerifier.AssertExpectations(t)
})
}
}
func TestVtpmverify(t *testing.T) {
tempDir := t.TempDir()
attestation := &tpmAttest.Attestation{
Quotes: []*tpm.Quote{
{
Quote: []byte("test quote"),
RawSig: []byte("test signature"),
},
},
}
binaryData, err := proto.Marshal(attestation)
require.NoError(t, err)
attestationFile := filepath.Join(tempDir, "vtpm_attestation.pb")
require.NoError(t, os.WriteFile(attestationFile, binaryData, 0o644))
tests := []struct {
name string
args []string
setupMock func(*mocks.Verifier)
expectErr bool
}{
{
name: "successful verification",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifVTpmAttestation", mock.Anything, mock.Anything).Return(nil)
},
expectErr: false,
},
{
name: "verification failure",
args: []string{attestationFile},
setupMock: func(m *mocks.Verifier) {
m.On("VerifVTpmAttestation", mock.Anything, mock.Anything).Return(fmt.Errorf("verification failed"))
},
expectErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
format = FormatBinaryPB
mockVerifier := new(mocks.Verifier)
tt.setupMock(mockVerifier)
err := vtpmverify(tt.args, mockVerifier)
if tt.expectErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockVerifier.AssertExpectations(t)
})
}
}
func uint32Ptr(v uint32) *uint32 {
return &v
}
func uint64Ptr(v uint64) *uint64 {
return &v
}
+258
View File
@@ -0,0 +1,258 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/hex"
"fmt"
"io"
"os"
"strings"
"github.com/absmach/magistrala/pkg/errors"
ccpb "github.com/google/go-tdx-guest/proto/checkconfig"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
var (
cfgTDX = &ccpb.Config{
RootOfTrust: &ccpb.RootOfTrust{},
Policy: &ccpb.Policy{HeaderPolicy: &ccpb.HeaderPolicy{}, TdQuoteBodyPolicy: &ccpb.TDQuoteBodyPolicy{}},
}
rtmrsS string
trustedRootS string
errNumberRtmrs = fmt.Errorf("expected 4 RTMRS values")
errDecodeRtmrs = fmt.Errorf("failed to decode RTMRS hex string")
errTrustedRootPath = fmt.Errorf("trusted root path must be a file, not a directory")
errNotAFile = fmt.Errorf("trusted root path must be a file")
)
func addTDXVerificationOptions(cmd *cobra.Command) *cobra.Command {
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.HeaderPolicy.QeVendorId,
"qe_vendor_id",
[]byte{},
"The expected QE_VENDOR_ID field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MrSeam,
"mr_seam",
[]byte{},
"The expected MR_SEAM field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.TdAttributes,
"td_attributes",
[]byte{},
"The expected TD_ATTRIBUTES field as a hex string. Must encode 8 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.Xfam,
"xfam",
[]byte{},
"The expected XFAM field as a hex string. Must encode 8 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MrTd,
"mr_td",
[]byte{},
"The expected MR_TD field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MrConfigId,
"mr_config_id",
[]byte{},
"The expected MR_CONFIG_ID field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MrOwnerConfig,
"mr_owner",
[]byte{},
"The expected MR_OWNER field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MrOwnerConfig,
"mr_config_owner",
[]byte{},
"The expected MR_OWNER_CONFIG field as a hex string. Must encode 48 bytes. Unchecked if unset.",
)
cmd.Flags().BytesHexVar(
&cfgTDX.Policy.TdQuoteBodyPolicy.MinimumTeeTcbSvn,
"minimum_tee_tcb_svn",
[]byte{},
"The minimum acceptable value for TEE_TCB_SVN field as a hex string. Must encode 16 bytes. Unchecked if unset.",
)
cmd.Flags().StringVar(
&rtmrsS,
"rtmrs",
"",
"Comma-separated hex strings representing expected values of RTMRS field. Expected 4 strings, either empty or each must encode 48 bytes. Unchecked if unset",
)
cmd.Flags().StringVar(
&trustedRootS,
"trusted_root",
"",
"Comma-separated paths to CA bundles for the Intel TDX. Must be in PEM format, Root CA certificate. If unset, uses embedded root certificate.",
)
cmd.Flags().Uint32Var(
&cfgTDX.Policy.HeaderPolicy.MinimumQeSvn,
"minimum_qe_svn",
0,
"The minimum acceptable value for QE_SVN field.",
)
cmd.Flags().Uint32Var(
&cfgTDX.Policy.HeaderPolicy.MinimumPceSvn,
"minimum_pce_svn",
0,
"The minimum acceptable value for PCE_SVN field.",
)
cmd.Flags().BoolVar(
&cfgTDX.RootOfTrust.GetCollateral,
"get_collateral",
false,
"If true, then permitted to download necessary collaterals for additional checks.",
)
return cmd
}
func parseRtmrs() ([][]byte, error) {
if rtmrsS == "" {
return nil, nil // No RTMRS provided, return nil
}
hexString := strings.Split(rtmrsS, ",")
if len(hexString) != 4 {
return nil, errNumberRtmrs
}
var result [][]byte
for _, hexStr := range hexString {
h, err := hex.DecodeString(strings.TrimSpace(hexStr))
if err != nil {
return nil, errors.Wrap(errDecodeRtmrs, err)
}
result = append(result, h)
}
return result, nil
}
func parseTrustedRoot() ([]string, error) {
if trustedRootS == "" {
return nil, nil // No trusted roots provided, return nil
}
roots := strings.Split(trustedRootS, ",")
var result []string
for _, root := range roots {
p := strings.TrimSpace(root)
state, err := os.Stat(p)
if err != nil {
return nil, errors.Wrap(errTrustedRootPath, err)
}
if state.IsDir() {
return nil, errNotAFile
}
result = append(result, p)
}
return result, nil
}
func parseTDXConfig() error {
if cfgString == "" {
return nil // No config provided, return nil
}
policyByte, err := os.ReadFile(cfgString)
if err != nil {
return err
}
if err := protojson.Unmarshal(policyByte, cfgTDX); err != nil {
return err
}
return nil
}
func validateTDXFlags() error {
if err := parseTDXConfig(); err != nil {
return err
}
rtrms, err := parseRtmrs()
if err != nil {
return err
}
if rtrms != nil {
cfgTDX.Policy.TdQuoteBodyPolicy.Rtmrs = rtrms
}
trustedRoots, err := parseTrustedRoot()
if err != nil {
return err
}
if trustedRoots != nil {
cfgTDX.RootOfTrust.CabundlePaths = trustedRoots
}
if err := validateTDXinput(); err != nil {
return err
}
return nil
}
func tdxVerify(reportFilePath string, verifier attestation.Verifier) error {
attestationFile = reportFilePath
input, err := openInputFile()
if err != nil {
return err
}
if closer, ok := input.(*os.File); ok {
defer closer.Close()
}
attestationBytes, err := io.ReadAll(input)
if err != nil {
return err
}
return verifier.VerifyAttestation(attestationBytes, reportData, nil)
}
func validateTDXinput() error {
if err := validateFieldLength("qe_vendor_id", cfgTDX.Policy.HeaderPolicy.QeVendorId, size16); err != nil {
return err
}
if err := validateFieldLength("mr_seam", cfgTDX.Policy.TdQuoteBodyPolicy.MrSeam, size48); err != nil {
return err
}
if err := validateFieldLength("td_attributes", cfgTDX.Policy.TdQuoteBodyPolicy.TdAttributes, size8); err != nil {
return err
}
if err := validateFieldLength("xfam", cfgTDX.Policy.TdQuoteBodyPolicy.Xfam, size8); err != nil {
return err
}
if err := validateFieldLength("mr_td", cfgTDX.Policy.TdQuoteBodyPolicy.MrTd, size48); err != nil {
return err
}
if err := validateFieldLength("mr_config_id", cfgTDX.Policy.TdQuoteBodyPolicy.MrConfigId, size48); err != nil {
return err
}
if err := validateFieldLength("mr_owner", cfgTDX.Policy.TdQuoteBodyPolicy.MrOwnerConfig, size48); err != nil {
return err
}
if err := validateFieldLength("mr_config_owner", cfgTDX.Policy.TdQuoteBodyPolicy.MrOwnerConfig, size48); err != nil {
return err
}
if err := validateFieldLength("minimum_tee_tcb_svn", cfgTDX.Policy.TdQuoteBodyPolicy.MinimumTeeTcbSvn, size16); err != nil {
return err
}
return nil
}
+707 -27
View File
@@ -4,10 +4,12 @@ package cli
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/absmach/magistrala/pkg/errors"
@@ -18,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
mmocks "github.com/ultravioletrs/cocos/pkg/attestation/cmdconfig/mocks"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/ultravioletrs/cocos/pkg/sdk/mocks"
@@ -209,7 +212,7 @@ func TestNewValidateAttestationValidationCmdDefaults(t *testing.T) {
cmd := cli.NewValidateAttestationValidationCmd()
assert.Equal(t, "validate", cmd.Use)
expectedMessage := fmt.Sprintf("Validate and verify attestation information. You can define the confidential computing cloud provider (%s, %s, %s; %s is the default) and can choose from 3 modes: %s, %s and %s. Default mode is %s.", CCNone, CCAzure, CCGCP, CCNone, SNP, VTPM, SNPvTPM, SNP)
expectedMessage := fmt.Sprintf("Validate and verify attestation information. You can define the confidential computing cloud provider (%s, %s, %s; %s is the default) and can choose from 4 modes: %s, %s, %s, and %s. Default mode is %s.", CCNone, CCAzure, CCGCP, CCNone, SNP, VTPM, SNPvTPM, TDX, SNP)
assert.Equal(t, expectedMessage, cmd.Short)
assert.Equal(t, fmt.Sprint(defaultMinimumTcb), cmd.Flag("minimum_tcb").Value.String())
@@ -311,26 +314,12 @@ func TestNewValidateAttestationValidationCmd(t *testing.T) {
})
}
type MockMeasurement struct {
mock.Mock
}
func (m *MockMeasurement) Run(igvmBinaryPath string) ([]byte, error) {
args := m.Called(igvmBinaryPath)
return nil, args.Error(0)
}
func (m *MockMeasurement) Stop() error {
args := m.Called()
return args.Error(0)
}
func TestNewMeasureCmd_RunSuccess(t *testing.T) {
cliInstance := &CLI{}
mockMeasurement := new(MockMeasurement)
mockMeasurement := new(mmocks.MeasurementProvider)
cliInstance.measurement = mockMeasurement
mockMeasurement.On("Run", "testfile.igvm").Return(nil)
mockMeasurement.On("Run", "testfile.igvm").Return([]byte{}, nil)
cmd := cliInstance.NewMeasureCmd("fake_binary_path")
buf := new(bytes.Buffer)
@@ -346,11 +335,11 @@ func TestNewMeasureCmd_RunSuccess(t *testing.T) {
func TestNewMeasureCmd_RunError(t *testing.T) {
cliInstance := &CLI{}
mockMeasurement := new(MockMeasurement)
mockMeasurement := new(mmocks.MeasurementProvider)
cliInstance.measurement = mockMeasurement
expectedError := errors.New("mocked measurement error")
mockMeasurement.On("Run", "testfile.igvm").Return(expectedError)
mockMeasurement.On("Run", "testfile.igvm").Return([]byte{}, expectedError)
cmd := cliInstance.NewMeasureCmd("fake_binary_path")
@@ -366,25 +355,34 @@ func TestNewMeasureCmd_RunError(t *testing.T) {
mockMeasurement.AssertExpectations(t)
}
func TestParseConfig(t *testing.T) {
func TestParseConfig1(t *testing.T) {
tmpfile, err := os.CreateTemp("", "attestation_policy.json")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())
cfgString = ""
err := parseConfig()
err = parseConfig()
assert.NoError(t, err)
assert.NotNil(t, cfg.RootOfTrust)
assert.NotNil(t, cfg.Policy)
cfgString = `{"rootOfTrust":{"product":"test_product"},"policy":{"minimumGuestSvn":1}}`
cfgString = tmpfile.Name()
_, err = tmpfile.WriteString(`{"rootOfTrust":{"product":"test_product"},"policy":{"minimumGuestSvn":1}}`)
require.NoError(t, err)
err = parseConfig()
assert.NoError(t, err)
assert.Equal(t, "test_product", cfg.RootOfTrust.Product)
assert.Equal(t, uint32(1), cfg.Policy.MinimumGuestSvn)
cfgString = `{"invalid_json"`
_, err = tmpfile.WriteString(`{"invalid_json"`)
require.NoError(t, err)
err = parseConfig()
assert.Error(t, err)
}
func TestParseHashes(t *testing.T) {
func TestParseHashes1(t *testing.T) {
trustedAuthorHashes = []string{"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}
trustedIdKeyHashes = []string{"fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"}
@@ -435,7 +433,7 @@ func TestParseFiles(t *testing.T) {
assert.Error(t, err)
}
func TestParseUints(t *testing.T) {
func TestParseUints1(t *testing.T) {
stepping = "10"
platformInfo = "0xFF"
@@ -460,7 +458,7 @@ func TestParseUints(t *testing.T) {
assert.Error(t, err)
}
func TestValidateInput(t *testing.T) {
func TestValidateInput1(t *testing.T) {
cfg = check.Config{}
if cfg.Policy == nil {
cfg.Policy = &check.Policy{}
@@ -485,7 +483,7 @@ func TestValidateInput(t *testing.T) {
assert.Error(t, err)
}
func TestGetBase(t *testing.T) {
func TestGetBase1(t *testing.T) {
assert.Equal(t, 16, getBase("0xFF"))
assert.Equal(t, 8, getBase("0o77"))
assert.Equal(t, 2, getBase("0b1010"))
@@ -707,3 +705,685 @@ func TestDecodeJWTToJSON(t *testing.T) {
})
}
}
func setupTestEnvironment() func() {
originalMode := mode
originalCfgString := cfgString
originalTimeout := timeout
originalMaxRetryDelay := maxRetryDelay
originalPlatformInfo := platformInfo
originalStepping := stepping
originalTrustedAuthorKeys := trustedAuthorKeys
originalTrustedAuthorHashes := trustedAuthorHashes
originalTrustedIdKeys := trustedIdKeys
originalTrustedIdKeyHashes := trustedIdKeyHashes
originalAttestationFile := attestationFile
originalAttestationRaw := attestationRaw
originalOutput := output
originalNonce := nonce
originalFormat := format
originalTeeNonce := teeNonce
originalTokenNonce := tokenNonce
originalGetTextProtoAttestationReport := getTextProtoAttestationReport
originalGetAzureTokenJWT := getAzureTokenJWT
originalCloud := cloud
originalReportData := reportData
originalCheckCrl := checkCrl
mode = ""
cfgString = ""
timeout = 0
maxRetryDelay = 0
platformInfo = ""
stepping = ""
trustedAuthorKeys = []string{}
trustedAuthorHashes = []string{}
trustedIdKeys = []string{}
trustedIdKeyHashes = []string{}
attestationFile = ""
attestationRaw = []byte{}
output = ""
nonce = []byte{}
format = ""
teeNonce = []byte{}
tokenNonce = []byte{}
getTextProtoAttestationReport = false
getAzureTokenJWT = false
cloud = ""
reportData = []byte{}
checkCrl = false
return func() {
mode = originalMode
cfgString = originalCfgString
timeout = originalTimeout
maxRetryDelay = originalMaxRetryDelay
platformInfo = originalPlatformInfo
stepping = originalStepping
trustedAuthorKeys = originalTrustedAuthorKeys
trustedAuthorHashes = originalTrustedAuthorHashes
trustedIdKeys = originalTrustedIdKeys
trustedIdKeyHashes = originalTrustedIdKeyHashes
attestationFile = originalAttestationFile
attestationRaw = originalAttestationRaw
output = originalOutput
nonce = originalNonce
format = originalFormat
teeNonce = originalTeeNonce
tokenNonce = originalTokenNonce
getTextProtoAttestationReport = originalGetTextProtoAttestationReport
getAzureTokenJWT = originalGetAzureTokenJWT
cloud = originalCloud
reportData = originalReportData
checkCrl = originalCheckCrl
}
}
func createTempFile(t *testing.T, content []byte) string {
tmpfile, err := os.CreateTemp("", "test_*.bin")
require.NoError(t, err)
defer tmpfile.Close()
_, err = tmpfile.Write(content)
require.NoError(t, err)
return tmpfile.Name()
}
func TestNewAttestationCmdEdgeCases(t *testing.T) {
tests := []struct {
name string
args []string
expectedOutput string
hasSubcommands bool
}{
{
name: "no arguments shows help",
args: []string{},
expectedOutput: "Get and validate attestations",
hasSubcommands: true,
},
{
name: "help flag shows usage",
args: []string{"--help"},
expectedOutput: "Get and validate attestations",
hasSubcommands: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSDK := new(mocks.SDK)
cli := &CLI{agentSDK: mockSDK}
cmd := cli.NewAttestationCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs(tt.args)
err := cmd.Execute()
assert.NoError(t, err)
output := buf.String()
assert.Contains(t, output, tt.expectedOutput)
if tt.hasSubcommands {
assert.Contains(t, output, "Get and validate attestations")
assert.Contains(t, output, "Usage:")
assert.Contains(t, output, "Flags:")
}
})
}
}
func TestGetAttestationCmdEdgeCases(t *testing.T) {
defer setupTestEnvironment()()
testCases := []struct {
name string
args []string
setupMock func(*mocks.SDK)
expectedErr string
expectedOut string
}{
{
name: "no arguments provided",
args: []string{},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "accepts 1 arg(s), received 0",
},
{
name: "too many arguments",
args: []string{"snp", "extra"},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "accepts 1 arg(s), received 2",
},
{
name: "invalid attestation type",
args: []string{"invalid-type"},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "Bad attestation type",
},
{
name: "SNP with missing TEE nonce",
args: []string{"snp"},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "TEE nonce must be defined for SEV-SNP attestation",
},
{
name: "vTPM with missing nonce",
args: []string{"vtpm"},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "vTPM nonce must be defined for vTPM attestation",
},
{
name: "Azure token with missing token nonce",
args: []string{"azure-token"},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "Token nonce must be defined for Azure attestation",
},
{
name: "TEE nonce too large",
args: []string{"snp", "--tee", hex.EncodeToString(bytes.Repeat([]byte{0x00}, quoteprovider.Nonce+1))},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "nonce must be a hex encoded string of length lesser or equal 64 bytes",
},
{
name: "vTPM nonce too large",
args: []string{"vtpm", "--vtpm", hex.EncodeToString(bytes.Repeat([]byte{0x00}, vtpm.Nonce+1))},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "vTPM nonce must be a hex encoded string of length lesser or equal 32 bytes",
},
{
name: "Token nonce too large",
args: []string{"azure-token", "--token", hex.EncodeToString(bytes.Repeat([]byte{0x00}, vtpm.Nonce+1))},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "vTPM nonce must be a hex encoded string of length lesser or equal 32 bytes",
},
{
name: "successful TDX attestation",
args: []string{"tdx", "--tee", hex.EncodeToString(bytes.Repeat([]byte{0x00}, quoteprovider.Nonce))},
setupMock: func(sdk *mocks.SDK) {
sdk.On("Attestation", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(nil).Run(func(args mock.Arguments) {
if _, err := args.Get(4).(*os.File).Write([]byte("mock tdx attestation")); err != nil {
t.Fatalf("Failed to write to attestation file: %v", err)
}
})
},
expectedOut: "Fetching TDX attestation report",
},
{
name: "file creation error",
args: []string{"snp", "--tee", hex.EncodeToString(bytes.Repeat([]byte{0x00}, quoteprovider.Nonce))},
setupMock: func(sdk *mocks.SDK) {
},
expectedErr: "Error creating attestation file",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
os.Remove(attestationFilePath)
os.Remove(azureAttestResultFilePath)
os.Remove(azureAttestTokenFilePath)
defer func() {
os.Remove(attestationFilePath)
os.Remove(azureAttestResultFilePath)
os.Remove(azureAttestTokenFilePath)
}()
mockSDK := new(mocks.SDK)
cli := &CLI{agentSDK: mockSDK}
tc.setupMock(mockSDK)
if tc.name == "file creation error" {
err := os.Mkdir(attestationFilePath, 0o755)
require.NoError(t, err)
defer os.Remove(attestationFilePath)
}
cmd := cli.NewGetAttestationCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs(tc.args)
err := cmd.Execute()
output := buf.String()
if tc.expectedErr != "" {
assert.Contains(t, output, tc.expectedErr)
} else {
assert.NoError(t, err)
if tc.expectedOut != "" {
assert.Contains(t, output, tc.expectedOut)
}
}
})
}
}
func TestFileOperations(t *testing.T) {
defer setupTestEnvironment()()
t.Run("openInputFile", func(t *testing.T) {
attestationFile = ""
reader, err := openInputFile()
assert.Error(t, err)
assert.Equal(t, errEmptyFile, err)
assert.Nil(t, reader)
tempFile := createTempFile(t, []byte("test content"))
defer os.Remove(tempFile)
attestationFile = tempFile
reader, err = openInputFile()
assert.NoError(t, err)
assert.NotNil(t, reader)
if file, ok := reader.(*os.File); ok {
file.Close()
}
attestationFile = "non-existent-file.bin"
reader, err = openInputFile()
assert.Error(t, err)
assert.Nil(t, reader)
})
t.Run("createOutputFile", func(t *testing.T) {
output = ""
writer, err := createOutputFile()
assert.NoError(t, err)
assert.Equal(t, os.Stdout, writer)
tempDir := t.TempDir()
output = filepath.Join(tempDir, "test_output.txt")
writer, err = createOutputFile()
assert.NoError(t, err)
assert.NotNil(t, writer)
if file, ok := writer.(*os.File); ok {
file.Close()
}
output = "/invalid/path/file.txt"
writer, err = createOutputFile()
assert.Error(t, err)
assert.Nil(t, writer)
})
}
func TestValidationFunctions(t *testing.T) {
t.Run("validateFieldLength", func(t *testing.T) {
tests := []struct {
name string
fieldName string
field []byte
expectedLength int
expectError bool
}{
{
name: "nil field",
fieldName: "test",
field: nil,
expectedLength: 32,
expectError: false,
},
{
name: "correct length",
fieldName: "test",
field: make([]byte, 32),
expectedLength: 32,
expectError: false,
},
{
name: "incorrect length",
fieldName: "test",
field: make([]byte, 16),
expectedLength: 32,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateFieldLength(tt.fieldName, tt.field, tt.expectedLength)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.fieldName)
} else {
assert.NoError(t, err)
}
})
}
})
}
func TestDecodeJWTToJSONEdgeCases(t *testing.T) {
tests := []struct {
name string
input []byte
expected string
hasError bool
}{
{
name: "empty input",
input: []byte(""),
hasError: true,
},
{
name: "single part",
input: []byte("onlyonepart"),
hasError: true,
},
{
name: "invalid base64 in header",
input: []byte("invalid@base64.validpart"),
hasError: true,
},
{
name: "invalid base64 in payload",
input: []byte("eyJhbGciOiJIUzI1NiJ9.invalid@base64"),
hasError: true,
},
{
name: "invalid JSON in header",
input: []byte("bm90anNvbg.eyJzdWIiOiJ0ZXN0In0"),
hasError: true,
},
{
name: "invalid JSON in payload",
input: []byte("eyJhbGciOiJIUzI1NiJ9.bm90anNvbg"),
hasError: true,
},
{
name: "valid JWT with padding",
input: []byte("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.signature"),
expected: `{
"header": {
"alg": "HS256"
},
"payload": {
"sub": "test"
}
}`,
hasError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := decodeJWTToJSON(tt.input)
if tt.hasError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
if tt.expected != "" {
assert.JSONEq(t, tt.expected, string(result))
}
}
})
}
}
func TestMeasureCmdEdgeCases(t *testing.T) {
tests := []struct {
name string
args []string
mockSetup func(*mmocks.MeasurementProvider)
expectedError string
expectedOut string
}{
{
name: "no arguments",
args: []string{},
mockSetup: func(m *mmocks.MeasurementProvider) {
},
expectedError: "requires at least 1 arg(s), only received 0",
},
{
name: "single line output success",
args: []string{"test.igvm"},
mockSetup: func(m *mmocks.MeasurementProvider) {
m.On("Run", "test.igvm").Return([]byte("ABCDEF123456"), nil)
},
expectedOut: "",
},
{
name: "multi-line output error",
args: []string{"test.igvm"},
mockSetup: func(m *mmocks.MeasurementProvider) {
m.On("Run", "test.igvm").Return([]byte("line1\nline2\nERROR: something went wrong"), nil)
},
expectedError: "ERROR: something went wrong",
},
{
name: "measurement run error",
args: []string{"test.igvm"},
mockSetup: func(m *mmocks.MeasurementProvider) {
m.On("Run", "test.igvm").Return(nil, errors.New("measurement failed"))
},
expectedError: "measurement failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockMeasurement := new(mmocks.MeasurementProvider)
tt.mockSetup(mockMeasurement)
cli := &CLI{measurement: mockMeasurement}
cmd := cli.NewMeasureCmd("fake_binary_path")
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
cmd.SetArgs(tt.args)
err := cmd.Execute()
if tt.expectedError != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError)
} else {
assert.NoError(t, err)
if tt.expectedOut != "" {
assert.Contains(t, buf.String(), tt.expectedOut)
}
}
mockMeasurement.AssertExpectations(t)
})
}
}
func TestValidateAttestationValidationCmdPreRunE(t *testing.T) {
tests := []struct {
name string
args []string
flags map[string]string
expectedErr string
}{
{
name: "no file path provided",
args: []string{},
flags: map[string]string{"mode": "snp"},
expectedErr: "please pass the attestation report file path",
},
{
name: "multiple file paths",
args: []string{"file1.bin", "file2.bin"},
flags: map[string]string{"mode": "snp"},
expectedErr: "please pass the attestation report file path",
},
{
name: "unknown mode",
args: []string{"test.bin"},
flags: map[string]string{"mode": "unknown"},
expectedErr: "unknown mode: unknown",
},
{
name: "SNP mode missing report_data",
args: []string{"test.bin"},
flags: map[string]string{"mode": "snp"},
expectedErr: "",
},
{
name: "SNP mode missing product",
args: []string{"test.bin"},
flags: map[string]string{"mode": "snp", "report_data": "123"},
expectedErr: "",
},
{
name: "vTPM mode missing nonce",
args: []string{"test.bin"},
flags: map[string]string{"mode": "vtpm"},
expectedErr: "",
},
{
name: "SNP-vTPM mode missing required flags",
args: []string{"test.bin"},
flags: map[string]string{"mode": "snp-vtpm"},
expectedErr: "",
},
{
name: "TDX mode missing report_data",
args: []string{"test.bin"},
flags: map[string]string{"mode": "tdx"},
expectedErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli := &CLI{}
cmd := cli.NewValidateAttestationValidationCmd()
for key, value := range tt.flags {
if err := cmd.Flags().Set(key, value); err != nil {
}
}
err := cmd.PreRunE(cmd, tt.args)
if tt.expectedErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedErr)
} else {
assert.NoError(t, err)
}
})
}
}
func TestCloudProviderConfigurations(t *testing.T) {
defer setupTestEnvironment()()
tests := []struct {
name string
cloud string
expectedType string
}{
{
name: "none cloud provider",
cloud: CCNone,
expectedType: "vtpm",
},
{
name: "azure cloud provider",
cloud: CCAzure,
expectedType: "azure",
},
{
name: "gcp cloud provider",
cloud: CCGCP,
expectedType: "vtpm",
},
{
name: "default cloud provider",
cloud: "",
expectedType: "vtpm",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli := &CLI{}
cmd := cli.NewValidateAttestationValidationCmd()
if err := cmd.Flags().Set("cloud", tt.cloud); err != nil {
t.Fatalf("Failed to set cloud flag: %v", err)
}
cloud, _ := cmd.Flags().GetString("cloud")
assert.Equal(t, tt.cloud, cloud)
assert.Contains(t, cmd.Short, tt.cloud)
})
}
}
func TestFileOperationErrors(t *testing.T) {
defer setupTestEnvironment()()
t.Run("file close error handling", func(t *testing.T) {
tempFile := createTempFile(t, []byte("test content"))
defer os.Remove(tempFile)
assert.True(t, true)
})
t.Run("file write error handling", func(t *testing.T) {
tempFile := createTempFile(t, []byte("test content"))
defer os.Remove(tempFile)
err := os.Chmod(tempFile, 0o444)
require.NoError(t, err)
err = os.WriteFile(tempFile, []byte("new content"), 0o644)
assert.Error(t, err)
})
t.Run("file read error handling", func(t *testing.T) {
tempDir := t.TempDir()
_, err := os.ReadFile(tempDir)
assert.Error(t, err)
})
}
func TestContextCancellation(t *testing.T) {
defer setupTestEnvironment()()
mockSDK := new(mocks.SDK)
cli := &CLI{agentSDK: mockSDK}
ctx, cancel := context.WithCancel(context.Background())
cancel()
mockSDK.On("Attestation", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(context.Canceled)
cmd := cli.NewGetAttestationCmd()
cmd.SetContext(ctx)
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
teeNonceHex := hex.EncodeToString(bytes.Repeat([]byte{0x00}, quoteprovider.Nonce))
cmd.SetArgs([]string{"snp", "--tee", teeNonceHex})
err := cmd.Execute()
assert.NoError(t, err)
assert.Contains(t, buf.String(), "Failed to get attestation due to error")
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/google/go-sev-guest/verify/trust"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
const (
@@ -28,7 +29,7 @@ func (cli *CLI) NewCABundleCmd(fileSavePath string) *cobra.Command {
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
attestationConfiguration := attestation.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &attestation.PcrConfig{}}
err := attestation.ReadAttestationPolicy(args[0], &attestationConfiguration)
err := vtpm.ReadPolicy(args[0], &attestationConfiguration)
if err != nil {
printError(cmd, "Error while reading manifest: %v ❌ ", err)
return
+169
View File
@@ -0,0 +1,169 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"bytes"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/ultravioletrs/cocos/pkg/sdk/mocks"
)
func TestCLI_NewIMAMeasurementsCmd(t *testing.T) {
testCases := []struct {
name string
args []string
connectErr error
mockIMAData string
mockError error
expectedFilename string
expectedOutput []string
expectedError []string
shouldCreateFile bool
fileCreationError bool
invalidDigestData bool
setupCustomFile func(filename string) error
}{
{
name: "successful_retrieval_default_filename",
args: []string{},
connectErr: nil,
mockIMAData: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
mockError: nil,
expectedFilename: imaMeasurementsFilename,
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "Linux IMA measurements file retrieved and saved successfully", "PCR10 = 0000000000000000000000000000000000000000", "Measurements file verified!"},
shouldCreateFile: true,
},
{
name: "successful_retrieval_custom_filename",
args: []string{"custom_ima_file.txt"},
connectErr: nil,
mockIMAData: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
mockError: nil,
expectedFilename: "custom_ima_file.txt",
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "custom_ima_file.txt", "Measurements file verified!"},
shouldCreateFile: true,
},
{
name: "connection_error",
args: []string{},
connectErr: fmt.Errorf("connection failed"),
expectedError: []string{"Failed to connect to agent: connection failed ❌"},
},
{
name: "file_creation_error",
args: []string{"/invalid/path/file.txt"},
connectErr: nil,
fileCreationError: true,
expectedError: []string{"Error creating imaMeasurements file:"},
},
{
name: "sdk_error",
args: []string{},
connectErr: nil,
mockError: fmt.Errorf("SDK communication failed"),
expectedError: []string{"Error retrieving Linux IMA measurements file: SDK communication failed ❌"},
},
{
name: "verification_failure_wrong_pcr",
args: []string{},
connectErr: nil,
mockIMAData: "10 9999999999999999999999999999999999999999 ima-ng sha1:0000000000000000000000000000000000000000 /usr/bin/test",
mockError: nil,
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "Linux IMA measurements file retrieved and saved successfully"},
expectedError: []string{"Measurements file not verified ❌"},
shouldCreateFile: true,
},
{
name: "empty_measurements_file",
args: []string{},
connectErr: nil,
mockIMAData: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
mockError: nil,
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "Linux IMA measurements file retrieved and saved successfully", "Measurements file verified!"},
shouldCreateFile: true,
},
{
name: "measurements_with_non_pcr10_entries",
args: []string{},
connectErr: nil,
mockIMAData: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
mockError: nil,
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "Linux IMA measurements file retrieved and saved successfully", "Measurements file verified!"},
shouldCreateFile: true,
},
{
name: "measurements_with_zero_digest_replacement",
args: []string{},
connectErr: nil,
mockIMAData: "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
mockError: nil,
expectedOutput: []string{"⏳ Retrieving computation Linux IMA measurements file", "Linux IMA measurements file retrieved and saved successfully", "Measurements file verified!"},
shouldCreateFile: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockSDK := new(mocks.SDK)
cli := &CLI{
agentSDK: mockSDK,
connectErr: tc.connectErr,
}
if tc.connectErr == nil && !tc.fileCreationError {
mockSDK.On("IMAMeasurements", mock.Anything, mock.Anything).Return([]byte(tc.mockIMAData), tc.mockError)
}
cmd := cli.NewIMAMeasurementsCmd()
var output bytes.Buffer
cmd.SetOut(&output)
cmd.SetErr(&output)
expectedFilename := tc.expectedFilename
if expectedFilename == "" {
if len(tc.args) > 0 {
expectedFilename = tc.args[0]
} else {
expectedFilename = imaMeasurementsFilename
}
}
if tc.setupCustomFile != nil {
err := tc.setupCustomFile(expectedFilename)
assert.NoError(t, err)
}
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err, "Command execution failed")
outputStr := output.String()
for _, expectedMsg := range tc.expectedOutput {
assert.Contains(t, outputStr, expectedMsg, "Expected output message not found")
}
for _, expectedErr := range tc.expectedError {
assert.Contains(t, outputStr, expectedErr, "Expected error message not found")
}
if tc.shouldCreateFile && tc.connectErr == nil && !tc.fileCreationError && tc.mockError == nil {
if _, err := os.Stat(expectedFilename); err == nil {
os.Remove(expectedFilename)
}
}
if tc.connectErr == nil && !tc.fileCreationError {
mockSDK.AssertExpectations(t)
}
})
}
}
+10 -6
View File
@@ -38,9 +38,11 @@ func (c *CLI) NewCreateVMCmd() *cobra.Command {
Example: `create-vm`,
Args: cobra.ExactArgs(0),
Run: func(cmd *cobra.Command, args []string) {
if err := c.InitializeManagerClient(cmd); err != nil {
printError(cmd, "Failed to connect to manager: %v ❌ ", c.connectErr)
return
if c.managerClient == nil || c.connectErr != nil {
if err := c.InitializeManagerClient(cmd); err != nil {
printError(cmd, "Failed to connect to manager: %v ❌ ", c.connectErr)
return
}
}
defer c.Close()
@@ -74,7 +76,7 @@ func (c *CLI) NewCreateVMCmd() *cobra.Command {
cmd.Flags().StringVar(&agentCVMServerCA, serverCA, "", "CVM server CA")
cmd.Flags().StringVar(&agentCVMClientKey, clientKey, "", "CVM client key")
cmd.Flags().StringVar(&agentCVMClientCrt, clientCrt, "", "CVM client crt")
cmd.Flags().StringVar(&agentCVMCaUrl, agentCVMCaUrl, "", "CVM CA service URL")
cmd.Flags().StringVar(&agentCVMCaUrl, caUrl, "", "CVM CA service URL")
cmd.Flags().StringVar(&agentLogLevel, logLevel, "", "Agent Log level")
cmd.Flags().DurationVar(&ttl, ttlFlag, 0, "TTL for the VM")
if err := cmd.MarkFlagRequired(serverURL); err != nil {
@@ -92,8 +94,10 @@ func (c *CLI) NewRemoveVMCmd() *cobra.Command {
Example: `remove-vm <cvm_id>`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if err := c.InitializeManagerClient(cmd); err == nil {
defer c.Close()
if c.managerClient == nil || c.connectErr != nil {
if err := c.InitializeManagerClient(cmd); err == nil {
defer c.Close()
}
}
if c.connectErr != nil {
+600
View File
@@ -0,0 +1,600 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"bytes"
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/manager"
"github.com/ultravioletrs/cocos/manager/mocks"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestCLI_NewCreateVMCmd(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.ManagerServiceClient)
setupCLI func(*CLI)
setupFiles func(string) error
flags map[string]string
expectedOutput string
expectedError string
expectError bool
}{
{
name: "successful VM creation with all flags",
setupMock: func(m *mocks.ManagerServiceClient) {
m.On("CreateVm", mock.Anything, mock.MatchedBy(func(req *manager.CreateReq) bool {
return req.AgentCvmServerUrl == "https://server.com" &&
req.AgentLogLevel == "debug" &&
req.AgentCvmCaUrl == "https://ca.com" &&
req.Ttl == "1h0m0s" &&
string(req.AgentCvmServerCaCert) == "ca-cert-content" &&
string(req.AgentCvmClientKey) == "client-key-content" &&
string(req.AgentCvmClientCert) == "client-cert-content"
})).Return(&manager.CreateRes{
CvmId: "vm-123",
ForwardedPort: "8080",
}, nil)
},
setupCLI: func(cli *CLI) {
},
setupFiles: func(tmpDir string) error {
files := map[string]string{
"server-ca.pem": "ca-cert-content",
"client-key.pem": "client-key-content",
"client-crt.pem": "client-cert-content",
}
for filename, content := range files {
if err := os.WriteFile(filepath.Join(tmpDir, filename), []byte(content), 0o644); err != nil {
return err
}
}
return nil
},
flags: map[string]string{
"server-url": "https://server.com",
"server-ca": "server-ca.pem",
"client-key": "client-key.pem",
"client-crt": "client-crt.pem",
"ca-url": "https://ca.com",
"log-level": "debug",
"ttl": "1h",
},
expectedOutput: "✅ Virtual machine created successfully with id vm-123 and port 8080",
expectError: false,
},
{
name: "successful VM creation with minimal flags",
setupMock: func(m *mocks.ManagerServiceClient) {
m.On("CreateVm", mock.Anything, mock.MatchedBy(func(req *manager.CreateReq) bool {
return req.AgentCvmServerUrl == "https://server.com" &&
req.AgentLogLevel == "" &&
req.AgentCvmCaUrl == "" &&
req.Ttl == "" &&
len(req.AgentCvmServerCaCert) == 0 &&
len(req.AgentCvmClientKey) == 0 &&
len(req.AgentCvmClientCert) == 0
})).Return(&manager.CreateRes{
CvmId: "vm-456",
ForwardedPort: "9090",
}, nil)
},
setupCLI: func(cli *CLI) {
},
setupFiles: func(tmpDir string) error {
return nil // No files needed for minimal test
},
flags: map[string]string{
"server-url": "https://server.com",
},
expectedOutput: "✅ Virtual machine created successfully with id vm-456 and port 9090",
expectError: false,
},
{
name: "manager client initialization failure",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as initialization fails
},
setupCLI: func(cli *CLI) {
cli.connectErr = errors.New("connection failed")
},
setupFiles: func(tmpDir string) error {
return nil
},
flags: map[string]string{
"server-url": "https://server.com",
},
expectedError: "Failed to connect to manager: failed to connect to grpc server : failed to exit idle mode: passthrough: received empty target in Build() ❌",
expectError: true,
},
{
name: "certificate loading failure",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as cert loading fails
},
setupCLI: func(cli *CLI) {
},
setupFiles: func(tmpDir string) error {
return nil // Don't create the cert file
},
flags: map[string]string{
"server-url": "https://server.com",
"server-ca": "nonexistent-ca.pem",
},
expectedError: "Error loading certs:",
expectError: true,
},
{
name: "CreateVm API call failure",
setupMock: func(m *mocks.ManagerServiceClient) {
m.On("CreateVm", mock.Anything, mock.Anything).Return(nil, errors.New("API error"))
},
setupCLI: func(cli *CLI) {
},
setupFiles: func(tmpDir string) error {
return nil
},
flags: map[string]string{
"server-url": "https://server.com",
},
expectedError: "Error creating virtual machine: API error ❌",
expectError: true,
},
{
name: "missing required server-url flag",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as command validation fails
},
setupCLI: func(cli *CLI) {
},
setupFiles: func(tmpDir string) error {
return nil
},
flags: map[string]string{}, // No server-url flag
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "cli-test-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
oldDir, err := os.Getwd()
require.NoError(t, err)
err = os.Chdir(tmpDir)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Chdir(oldDir)
require.NoError(t, err)
})
err = tt.setupFiles(tmpDir)
require.NoError(t, err)
mockClient := new(mocks.ManagerServiceClient)
tt.setupMock(mockClient)
mockCLI := &CLI{
managerClient: mockClient,
}
tt.setupCLI(mockCLI)
cmd := mockCLI.NewCreateVMCmd()
for flag, value := range tt.flags {
err := cmd.Flags().Set(flag, value)
require.NoError(t, err)
}
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cmd.Execute()
if tt.expectError {
if tt.expectedError != "" {
assert.Contains(t, buf.String(), tt.expectedError)
}
} else {
assert.NoError(t, err)
if tt.expectedOutput != "" {
assert.Contains(t, buf.String(), tt.expectedOutput)
}
}
mockClient.AssertExpectations(t)
})
}
}
func TestCLI_NewRemoveVMCmd(t *testing.T) {
tests := []struct {
name string
setupMock func(*mocks.ManagerServiceClient)
setupCLI func(*CLI)
args []string
expectedOutput string
expectedError string
expectError bool
}{
{
name: "successful VM removal",
setupMock: func(m *mocks.ManagerServiceClient) {
m.On("RemoveVm", mock.Anything, &manager.RemoveReq{
CvmId: "vm-123",
}).Return(&emptypb.Empty{}, nil)
},
setupCLI: func(cli *CLI) {
},
args: []string{"vm-123"},
expectedOutput: "✅ Virtual machine removed successfully",
expectError: false,
},
{
name: "manager client initialization failure",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as initialization fails
},
setupCLI: func(cli *CLI) {
cli.connectErr = errors.New("connection failed")
},
args: []string{"vm-123"},
expectedError: "Failed to connect to manager: failed to connect to grpc server : failed to exit idle mode: passthrough: received empty target in Build() ❌",
expectError: true,
},
{
name: "RemoveVm API call failure",
setupMock: func(m *mocks.ManagerServiceClient) {
m.On("RemoveVm", mock.Anything, &manager.RemoveReq{
CvmId: "vm-456",
}).Return(nil, errors.New("removal failed"))
},
setupCLI: func(cli *CLI) {
},
args: []string{"vm-456"},
expectedError: "Error removing virtual machine: removal failed ❌",
expectError: true,
},
{
name: "missing VM ID argument",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as command validation fails
},
setupCLI: func(cli *CLI) {
},
args: []string{}, // No VM ID provided
expectError: true,
},
{
name: "too many arguments",
setupMock: func(m *mocks.ManagerServiceClient) {
// No expectations set as command validation fails
},
setupCLI: func(cli *CLI) {
},
args: []string{"vm-123", "extra-arg"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := new(mocks.ManagerServiceClient)
tt.setupMock(mockClient)
mockCLI := &CLI{
managerClient: mockClient,
}
tt.setupCLI(mockCLI)
cmd := mockCLI.NewRemoveVMCmd()
cmd.SetArgs(tt.args)
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err := cmd.Execute()
if tt.expectError {
if tt.expectedError != "" {
assert.Contains(t, buf.String(), tt.expectedError)
}
} else {
assert.NoError(t, err)
if tt.expectedOutput != "" {
assert.Contains(t, buf.String(), tt.expectedOutput)
}
}
mockClient.AssertExpectations(t)
})
}
}
func TestFileReader(t *testing.T) {
tests := []struct {
name string
setupFile func(string) (string, error)
path string
expectedResult []byte
expectError bool
}{
{
name: "successful file read",
setupFile: func(tmpDir string) (string, error) {
filePath := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(filePath, []byte("test content"), 0o644)
return filePath, err
},
expectedResult: []byte("test content"),
expectError: false,
},
{
name: "empty path returns nil",
setupFile: func(tmpDir string) (string, error) {
return "", nil
},
path: "",
expectedResult: nil,
expectError: false,
},
{
name: "nonexistent file returns error",
setupFile: func(tmpDir string) (string, error) {
return filepath.Join(tmpDir, "nonexistent.txt"), nil
},
expectedResult: nil,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "fileReader-test-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
filePath, err := tt.setupFile(tmpDir)
require.NoError(t, err)
if tt.path != "" {
filePath = tt.path
}
result, err := fileReader(filePath)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedResult, result)
}
})
}
}
func TestLoadCerts(t *testing.T) {
tests := []struct {
name string
setupFiles func(string) error
setupGlobal func(string)
expectError bool
validate func(*testing.T, *manager.CreateReq)
}{
{
name: "successful cert loading with all files",
setupFiles: func(tmpDir string) error {
files := map[string]string{
"client.key": "client-key-content",
"client.crt": "client-cert-content",
"server.ca": "server-ca-content",
}
for filename, content := range files {
if err := os.WriteFile(filepath.Join(tmpDir, filename), []byte(content), 0o644); err != nil {
return err
}
}
return nil
},
setupGlobal: func(tmpDir string) {
agentCVMClientKey = filepath.Join(tmpDir, "client.key")
agentCVMClientCrt = filepath.Join(tmpDir, "client.crt")
agentCVMServerCA = filepath.Join(tmpDir, "server.ca")
},
expectError: false,
validate: func(t *testing.T, req *manager.CreateReq) {
assert.Equal(t, []byte("client-key-content"), req.AgentCvmClientKey)
assert.Equal(t, []byte("client-cert-content"), req.AgentCvmClientCert)
assert.Equal(t, []byte("server-ca-content"), req.AgentCvmServerCaCert)
},
},
{
name: "successful cert loading with empty paths",
setupFiles: func(tmpDir string) error {
return nil
},
setupGlobal: func(tmpDir string) {
agentCVMClientKey = ""
agentCVMClientCrt = ""
agentCVMServerCA = ""
},
expectError: false,
validate: func(t *testing.T, req *manager.CreateReq) {
assert.Nil(t, req.AgentCvmClientKey)
assert.Nil(t, req.AgentCvmClientCert)
assert.Nil(t, req.AgentCvmServerCaCert)
},
},
{
name: "client key file read error",
setupFiles: func(tmpDir string) error {
return nil // Don't create client key file
},
setupGlobal: func(tmpDir string) {
agentCVMClientKey = filepath.Join(tmpDir, "nonexistent.key")
agentCVMClientCrt = ""
agentCVMServerCA = ""
},
expectError: true,
},
{
name: "client cert file read error",
setupFiles: func(tmpDir string) error {
// Create client key but not cert
return os.WriteFile(filepath.Join(tmpDir, "client.key"), []byte("key-content"), 0o644)
},
setupGlobal: func(tmpDir string) {
agentCVMClientKey = filepath.Join(tmpDir, "client.key")
agentCVMClientCrt = filepath.Join(tmpDir, "nonexistent.crt")
agentCVMServerCA = ""
},
expectError: true,
},
{
name: "server CA file read error",
setupFiles: func(tmpDir string) error {
files := map[string]string{
"client.key": "client-key-content",
"client.crt": "client-cert-content",
}
for filename, content := range files {
if err := os.WriteFile(filepath.Join(tmpDir, filename), []byte(content), 0o644); err != nil {
return err
}
}
return nil
},
setupGlobal: func(tmpDir string) {
agentCVMClientKey = filepath.Join(tmpDir, "client.key")
agentCVMClientCrt = filepath.Join(tmpDir, "client.crt")
agentCVMServerCA = filepath.Join(tmpDir, "nonexistent.ca")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "loadCerts-test-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
err = tt.setupFiles(tmpDir)
require.NoError(t, err)
// Store original global variables
origClientKey := agentCVMClientKey
origClientCrt := agentCVMClientCrt
origServerCA := agentCVMServerCA
// Setup global variables for test
tt.setupGlobal(tmpDir)
// Restore original values after test
defer func() {
agentCVMClientKey = origClientKey
agentCVMClientCrt = origClientCrt
agentCVMServerCA = origServerCA
}()
result, err := loadCerts()
if tt.expectError {
assert.Error(t, err)
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
if tt.validate != nil {
tt.validate(t, result)
}
}
})
}
}
func TestCommandCreation(t *testing.T) {
cli := &CLI{}
t.Run("create-vm command creation", func(t *testing.T) {
cmd := cli.NewCreateVMCmd()
assert.NotNil(t, cmd)
assert.Equal(t, "create-vm", cmd.Use)
assert.Equal(t, "Create a new virtual machine", cmd.Short)
// Check that required flags are set
flag := cmd.Flags().Lookup("server-url")
assert.NotNil(t, flag)
// Note: We can't easily test MarkFlagRequired in unit tests
})
t.Run("remove-vm command creation", func(t *testing.T) {
cmd := cli.NewRemoveVMCmd()
assert.NotNil(t, cmd)
assert.Equal(t, "remove-vm", cmd.Use)
assert.Equal(t, "Remove a virtual machine", cmd.Short)
})
}
func TestTTLHandling(t *testing.T) {
tests := []struct {
name string
ttlInput string
expectedTTL time.Duration
expectError bool
}{
{
name: "valid duration",
ttlInput: "1h30m",
expectedTTL: time.Hour + 30*time.Minute,
expectError: false,
},
{
name: "zero duration",
ttlInput: "0",
expectedTTL: 0,
expectError: false,
},
{
name: "empty string",
ttlInput: "",
expectedTTL: 0,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockCLI := &CLI{
managerClient: new(mocks.ManagerServiceClient),
}
cmd := mockCLI.NewCreateVMCmd()
if tt.ttlInput != "" {
err := cmd.Flags().Set("ttl", tt.ttlInput)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedTTL, ttl)
}
}
})
}
}
+3 -1
View File
@@ -62,5 +62,7 @@ func (c *CLI) InitializeManagerClient(cmd *cobra.Command) error {
}
func (c *CLI) Close() {
c.client.Close()
if c.client != nil {
c.client.Close()
}
}
+6 -3
View File
@@ -28,6 +28,7 @@ import (
agentlogger "github.com/ultravioletrs/cocos/internal/logger"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
cvmsgrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc/cvm"
@@ -98,11 +99,13 @@ func main() {
switch ccPlatform {
case attestation.SNP:
provider = vtpm.New(nil, false, uint(cfg.Vmpl), nil)
provider = vtpm.NewProvider(false, uint(cfg.Vmpl))
case attestation.SNPvTPM:
provider = vtpm.New(nil, true, uint(cfg.Vmpl), nil)
provider = vtpm.NewProvider(true, uint(cfg.Vmpl))
case attestation.Azure:
provider = azure.New(nil)
provider = azure.NewProvider()
case attestation.TDX:
provider = tdx.NewProvider()
case attestation.NoCC:
logger.Info("TEE device not found")
provider = &attestation.EmptyProvider{}
+13 -7
View File
@@ -92,16 +92,14 @@ func main() {
}
if qemuCfg.EnableTDX {
logger.Info("Manager started with TDX enabled")
logger.Info("Manager started with TDX enabled")
} else if qemuCfg.EnableSEVSNP {
logger.Info("Manager started with SEV-SNP enabled")
} else if qemuCfg.EnableSEV {
logger.Info("Manager started with SEV enabled")
} else {
logger.Info("Manager started without confidential computing support")
}
if err := env.ParseWithOptions(&qemuCfg, env.Options{Prefix: envPrefixQemu}); err != nil {
if err := env.ParseWithOptions(qemuCfg, env.Options{Prefix: envPrefixQemu}); err != nil {
logger.Error(fmt.Sprintf("failed to load QEMU configuration: %s", err))
exitCode = 1
return
@@ -116,13 +114,21 @@ func main() {
return
}
svc, err := newService(ctx, logger, tracer, *qemuCfg, cfg.AttestationPolicyBinary, cfg.IgvmMeasureBinary, cfg.PcrValues, cfg.EosVersion)
svc, err := newService(logger, tracer, *qemuCfg, cfg.AttestationPolicyBinary, cfg.IgvmMeasureBinary, cfg.PcrValues, cfg.EosVersion)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
defer func() {
if err := svc.Shutdown(); err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
}()
registerManagerServiceServer := func(srv *grpc.Server) {
reflection.Register(srv)
manager.RegisterManagerServiceServer(srv, managergrpc.NewServer(svc))
@@ -143,8 +149,8 @@ func main() {
}
}
func newService(ctx context.Context, logger *slog.Logger, tracer trace.Tracer, qemuCfg qemu.Config, attestationPolicyPath string, igvmMeasurementBinaryPath string, pcrValuesFilePath string, eosVersion string) (manager.Service, error) {
svc, err := manager.New(ctx, qemuCfg, attestationPolicyPath, igvmMeasurementBinaryPath, pcrValuesFilePath, logger, qemu.NewVM, eosVersion)
func newService(logger *slog.Logger, tracer trace.Tracer, qemuCfg qemu.Config, attestationPolicyPath string, igvmMeasurementBinaryPath string, pcrValuesFilePath string, eosVersion string) (manager.Service, error) {
svc, err := manager.New(qemuCfg, attestationPolicyPath, igvmMeasurementBinaryPath, pcrValuesFilePath, logger, qemu.NewVM, eosVersion)
if err != nil {
return nil, err
}
+3 -4
View File
@@ -40,9 +40,9 @@ MANAGER_QEMU_VIRTIO_NET_PCI_ADDR=0x2
MANAGER_QEMU_VIRTIO_NET_PCI_ROMFILE=
MANAGER_QEMU_DISK_IMG_KERNEL_FILE=/etc/cocos/bzImage
MANAGER_QEMU_DISK_IMG_ROOTFS_FILE=/etc/cocos/rootfs.cpio.gz
MANAGER_QEMU_SEV_ID=sev0
MANAGER_QEMU_SEV_CBITPOS=51
MANAGER_QEMU_SEV_REDUCED_PHYS_BITS=1
MANAGER_QEMU_SEV_SNP_ID=sev0
MANAGER_QEMU_SEV_SNP_CBITPOS=51
MANAGER_QEMU_SEV_SNP_REDUCED_PHYS_BITS=1
MANAGER_QEMU_HOST_DATA=
MANAGER_QEMU_TDX_ID=tdx0
MANAGER_QEMU_QUOTE_GENERATION_PORT=4050
@@ -52,7 +52,6 @@ MANAGER_QEMU_VSOCK_GUEST_CID=3
MANAGER_QEMU_VSOCK_VNC=0
MANAGER_QEMU_BIN_PATH=qemu-system-x86_64
MANAGER_QEMU_USE_SUDO=true
MANAGER_QEMU_ENABLE_SEV=false
MANAGER_QEMU_ENABLE_SEV_SNP=false
MANAGER_QEMU_ENABLE_TDX=false
MANAGER_QEMU_IGVM_FILE=/etc/cocos/coconut-qemu.igvm
+44 -44
View File
@@ -11,41 +11,42 @@ require (
github.com/go-kit/kit v0.13.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/go-sev-guest v0.13.0
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 // indirect
github.com/mdlayher/vsock v1.2.1
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
github.com/virtee/sev-snp-measure-go v0.0.0-20240530153610-e6e8dc9b6877
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
go.opentelemetry.io/otel/trace v1.36.0
golang.org/x/crypto v0.38.0
golang.org/x/sync v0.14.0
google.golang.org/grpc v1.72.1
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0
go.opentelemetry.io/otel/trace v1.37.0
golang.org/x/crypto v0.39.0
golang.org/x/sync v0.15.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
)
require (
cloud.google.com/go/storage v1.51.0
cloud.google.com/go/storage v1.55.0
github.com/caarlos0/env/v10 v10.0.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/gce-tcb-verifier v0.3.1
)
require (
cel.dev/expr v0.20.0 // indirect
cloud.google.com/go v0.118.3 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/iam v1.4.1 // indirect
cloud.google.com/go/monitoring v1.24.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.121.1 // indirect
cloud.google.com/go/auth v0.16.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
@@ -54,21 +55,21 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/gofrs/uuid/v5 v5.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/google/certificate-transparency-go v1.1.8 // indirect
github.com/google/go-attestation v0.5.1 // indirect
github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -78,58 +79,57 @@ require (
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/time v0.10.0 // indirect
google.golang.org/api v0.224.0 // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/time v0.11.0 // indirect
google.golang.org/api v0.235.0 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)
require (
github.com/absmach/certs v0.0.0-20250313105043-afbcda9a9963
github.com/absmach/certs v0.0.0-20250707105817-4c41124bba54
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/docker v28.0.4+incompatible
github.com/docker/docker v28.2.2+incompatible
github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc // indirect
github.com/google/go-tpm v0.9.3
github.com/google/go-tpm v0.9.5
github.com/google/go-tpm-tools v0.4.4
github.com/google/logger v1.1.1
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
golang.org/x/text v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+94 -94
View File
@@ -1,29 +1,29 @@
cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI=
cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.118.3 h1:jsypSnrE/w4mJysioGdMBg4MiW/hHx/sArFpaBWHdME=
cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc=
cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/iam v1.4.1 h1:cFC25Nv+u5BkTR/BT1tXdoF2daiVbZ1RLx2eqfQ9RMM=
cloud.google.com/go/iam v1.4.1/go.mod h1:2vUEJpUG3Q9p2UdsyksaKpDzlwOrnMzS30isdReIcLM=
cel.dev/expr v0.23.0 h1:wUb94w6OYQS4uXraxo9U+wUAs9jT47Xvl4iPgAwM2ss=
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.121.1 h1:S3kTQSydxmu1JfLRLpKtxRPA7rSrYPRPEUmL/PavVUw=
cloud.google.com/go v0.121.1/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw=
cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.5 h1:sD+t8DO8j4HKW4QfouCklg7ZC1qC4uzVZt8iz3uTW+Q=
cloud.google.com/go/longrunning v0.6.5/go.mod h1:Et04XK+0TTLKa5IPYryKf5DkpwImy6TluQ1QTLwlKmY=
cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM=
cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc=
cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw=
cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc=
cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE=
cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
@@ -34,8 +34,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/absmach/certs v0.0.0-20250313105043-afbcda9a9963 h1:kN6NwYWNyTMAmDAwAGuxVYw6t8TL50CB6Kw3jOHj6iw=
github.com/absmach/certs v0.0.0-20250313105043-afbcda9a9963/go.mod h1:I4SywlzOXtUfJwVHCyPOUdl8rEsby2cJX2mqA5A4MOw=
github.com/absmach/certs v0.0.0-20250707105817-4c41124bba54 h1:5pTDktMVvtJrQMY0d2vwBRFOgH0LVEoArwcaqrbuGQ4=
github.com/absmach/certs v0.0.0-20250707105817-4c41124bba54/go.mod h1:MGDO7iEXbtJCRKOwL34+HR8MQI2mo7l3wcz93e2Km68=
github.com/absmach/magistrala v0.15.1 h1:3Bk2hlyWcV591LxPYwlvRcyCXTfuZ1g/EkNmU+o3NNQ=
github.com/absmach/magistrala v0.15.1/go.mod h1:9pto6xuBt/IuCtZRdEha0iDQKNQ5tyNOjLXJgUiikYk=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -48,8 +48,12 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -60,8 +64,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok=
github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -84,8 +88,8 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
@@ -93,8 +97,8 @@ github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
@@ -103,8 +107,6 @@ github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -127,8 +129,8 @@ github.com/google/go-sev-guest v0.13.0 h1:DJB6ACdykyweMU0HGOp/TQ7cjsnbV2ecbYunu2
github.com/google/go-sev-guest v0.13.0/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760=
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A=
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU=
github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ=
@@ -139,14 +141,14 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g=
github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -155,8 +157,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -166,12 +166,12 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
@@ -191,8 +191,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
@@ -229,30 +229,30 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -262,8 +262,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -278,17 +278,17 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -317,10 +317,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -331,16 +331,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU=
google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo=
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+5 -6
View File
@@ -35,11 +35,11 @@ DISK_IMG_ROOTFS_FILE=
KERNEL_COMMAND_LINE="quiet console=null"
#### Sev Config
SEV_ID=sev0
SEV_CBIT_POS=51
SEV_REDUCED_PHYS_BITS=1
SEV_HOST_DATA=
#### Sev-Snp Config
SEV_SNP_ID=sev0
SEV_SNP_CBIT_POS=51
SEV_SNP_REDUCED_PHYS_BITS=1
SEV_SNP_HOST_DATA=
#### VSock Config
VSOCK_ID=vhost-vsock-pci0
@@ -47,7 +47,6 @@ VSOCK_GUEST_CID=3
BIN_PATH=qemu-system-x86_64
USE_SUDO=false
ENABLE_SEV=false
ENABLE_SEV_SNP=false
ENABLE_KVM=true
+8 -11
View File
@@ -74,21 +74,18 @@ construct_qemu_args() {
args+=("-device" "virtio-net-pci,disable-legacy=$VIRTIO_NET_PCI_DISABLE_LEGACY,iommu_platform=$VIRTIO_NET_PCI_IOMMU_PLATFORM,netdev=$NET_DEV_ID,addr=$VIRTIO_NET_PCI_ADDR,romfile=$VIRTIO_NET_PCI_ROMFILE")
args+=("-device" "vhost-vsock-pci,id=$VSOCK_ID,guest-cid=$VSOCK_GUEST_CID")
# SEV (if enabled)
if [ "$ENABLE_SEV" == "true" ] || [ "$ENABLE_SEV_SNP" == "true" ]; then
sev_type="sev-guest"
# SEV_SNP (if enabled)
if [ "$ENABLE_SEV_SNP" == "true" ]; then
kernel_hash=""
host_data=""
args+=("-machine" "confidential-guest-support=$SEV_ID,memory-backend=$MEM_ID")
args+=("-machine" "confidential-guest-support=$SEV_SNP_ID,memory-backend=$MEM_ID")
if [ "$ENABLE_SEV_SNP" == "true" ]; then
args+=("-bios" "$OVMF_CODE_FILE")
sev_type="sev-snp-guest"
args+=("-bios" "$OVMF_CODE_FILE")
sev_snp_type="sev-snp-guest"
if [ -n "$SEV_HOST_DATA" ]; then
host_data=",host-data=$SEV_HOST_DATA"
fi
if [ -n "$SEV_SNP_HOST_DATA" ]; then
host_data=",host-data=$SEV_SNP_HOST_DATA"
fi
if [ "$ENABLE_KERNEL_HASH" == "true" ]; then
@@ -96,7 +93,7 @@ construct_qemu_args() {
fi
args+=("-object" "memory-backend-memfd,id=$MEM_ID,size=$MEMORY_SIZE,share=true,prealloc=false")
args+=("-object" "$sev_type,id=$SEV_ID,cbitpos=$SEV_CBIT_POS,reduced-phys-bits=$SEV_REDUCED_PHYS_BITS$kernel_hash$host_data")
args+=("-object" "$sev_snp_type,id=$SEV_SNP_ID,cbitpos=$SEV_SNP_CBIT_POS,reduced-phys-bits=$SEV_SNP_REDUCED_PHYS_BITS$kernel_hash$host_data")
fi
# Disk image configuration
+51 -223
View File
@@ -4,36 +4,21 @@
package grpc
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log/slog"
"math/big"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
certs "github.com/absmach/certs"
certscli "github.com/absmach/certs/cli"
"github.com/absmach/certs/errors"
certssdk "github.com/absmach/certs/sdk"
agentgrpc "github.com/ultravioletrs/cocos/agent/api/grpc"
"github.com/ultravioletrs/cocos/agent/auth"
"github.com/ultravioletrs/cocos/internal/server"
"github.com/ultravioletrs/cocos/pkg/atls"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
@@ -58,16 +43,15 @@ const (
type Server struct {
server.BaseServer
mu sync.RWMutex
server *grpc.Server
health *health.Server
registerService serviceRegister
authSvc auth.Authenticator
health *health.Server
caUrl string
cvmId string
}
type csrReq struct {
CSR string `json:"csr,omitempty"`
started bool
stopped bool
}
type serviceRegister func(srv *grpc.Server)
@@ -94,6 +78,18 @@ func New(ctx context.Context, cancel context.CancelFunc, name string, config ser
}
func (s *Server) Start() error {
s.mu.Lock()
if s.started {
s.mu.Unlock()
return fmt.Errorf("server already started")
}
if s.stopped {
s.mu.Unlock()
return fmt.Errorf("server already stopped")
}
s.started = true
s.mu.Unlock()
errCh := make(chan error)
grpcServerOptions := []grpc.ServerOption{
grpc.StatsHandler(otelgrpc.NewServerHandler()),
@@ -106,23 +102,12 @@ func (s *Server) Start() error {
}
creds := grpc.Creds(insecure.NewCredentials())
var listener net.Listener
c := s.Config.GetBaseConfig()
if agCfg, ok := s.Config.(server.AgentConfig); ok && agCfg.AttestedTLS {
certificateBytes, privateKeyBytes, err := generateCertificatesForATLS(s.caUrl, s.cvmId)
if err != nil {
return fmt.Errorf("failed to create certificate: %w", err)
}
certificate, err := tls.X509KeyPair(certificateBytes, privateKeyBytes)
if err != nil {
return fmt.Errorf("falied due to invalid key pair: %w", err)
}
tlsConfig := &tls.Config{
ClientAuth: tls.NoClientCert,
Certificates: []tls.Certificate{certificate},
ClientAuth: tls.NoClientCert,
GetCertificate: atls.GetCertificate(s.caUrl, s.cvmId),
}
var mtls bool
@@ -162,15 +147,7 @@ func (s *Server) Start() error {
creds = grpc.Creds(credentials.NewTLS(tlsConfig))
listener, err = atls.Listen(
s.Address,
certificateBytes,
privateKeyBytes,
)
if err != nil {
return fmt.Errorf("failed to create Listener for aTLS: %w", err)
} else if mtls {
if mtls {
s.Logger.Info(fmt.Sprintf("%s service gRPC server listening at %s with Attested mTLS", s.Name, s.Address))
} else {
s.Logger.Info(fmt.Sprintf("%s service gRPC server listening at %s with Attested TLS", s.Name, s.Address))
@@ -226,32 +203,34 @@ func (s *Server) Start() error {
default:
s.Logger.Info(fmt.Sprintf("%s service gRPC server listening at %s with TLS cert %s and key %s", s.Name, s.Address, c.CertFile, c.KeyFile))
}
listener, err = net.Listen("tcp", s.Address)
if err != nil {
return fmt.Errorf("failed to listen on port %s: %w", s.Address, err)
}
default:
var err error
listener, err = net.Listen("tcp", s.Address)
if err != nil {
return fmt.Errorf("failed to listen on port %s: %w", s.Address, err)
}
s.Logger.Info(fmt.Sprintf("%s service gRPC server listening at %s without TLS", s.Name, s.Address))
}
}
listener, err := net.Listen("tcp", s.Address)
if err != nil {
return fmt.Errorf("failed to listen on port %s: %w", s.Address, err)
}
grpcServerOptions = append(grpcServerOptions, creds)
s.mu.Lock()
s.server = grpc.NewServer(grpcServerOptions...)
s.health = health.NewServer()
grpchealth.RegisterHealthServer(s.server, s.health)
s.registerService(s.server)
s.health.SetServingStatus(s.Name, grpchealth.HealthCheckResponse_SERVING)
s.mu.Unlock()
go func() {
errCh <- s.server.Serve(listener)
s.mu.RLock()
server := s.server
s.mu.RUnlock()
if server != nil {
errCh <- server.Serve(listener)
}
}()
select {
@@ -264,19 +243,33 @@ func (s *Server) Start() error {
}
func (s *Server) Stop() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.stopped {
return nil
}
s.stopped = true
defer s.Cancel()
c := make(chan bool)
go func() {
defer close(c)
s.health.Shutdown()
s.server.GracefulStop()
if s.health != nil {
s.health.Shutdown()
}
if s.server != nil {
s.server.GracefulStop()
}
}()
select {
case <-c:
case <-time.After(stopWaitTime):
}
s.Logger.Info(fmt.Sprintf("%s gRPC service shutdown at %s", s.Name, s.Address))
s.Logger.Info(fmt.Sprintf("%s gRPC service shutdown at %s", s.Name, s.Address))
return nil
}
@@ -312,168 +305,3 @@ func loadX509KeyPair(certfile, keyfile string) (tls.Certificate, error) {
return tls.X509KeyPair(cert, key)
}
func generateCertificatesForATLS(caUrl string, cvmId string) ([]byte, []byte, error) {
curve := elliptic.P256()
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate private/public key: %w", err)
}
var certDERBytes []byte
if caUrl == "" || cvmId == "" {
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(202403311),
Subject: pkix.Name{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
DERBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
}
certDERBytes = DERBytes
} else {
csrmd := certs.CSRMetadata{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
}
csr, err := certscli.CreateCSR(csrmd, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create CSR: %w", err)
}
csrData := string(csr.CSR)
r := csrReq{
CSR: csrData,
}
data, error := json.Marshal(r)
if error != nil {
return nil, nil, errors.NewSDKError(error)
}
notBefore := time.Now()
notAfter := time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay)
ttlString := notAfter.Sub(notBefore).String()
query := url.Values{}
query.Add("ttl", ttlString)
query_string := query.Encode()
certsEndpoint := "certs"
csrEndpoint := "csrs"
endpoint := fmt.Sprintf("%s/%s/%s", certsEndpoint, csrEndpoint, cvmId)
url := fmt.Sprintf("%s/%s?%s", caUrl, endpoint, query_string)
_, body, sdkerr := processRequest(http.MethodPost, url, data, nil, http.StatusOK)
if sdkerr != nil {
return nil, nil, errors.NewSDKError(sdkerr)
}
var cert certssdk.Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return nil, nil, errors.NewSDKError(err)
}
cleanCertificateString := strings.ReplaceAll(cert.Certificate, "\\n", "\n")
block, rest := pem.Decode([]byte(cleanCertificateString))
if len(rest) != 0 {
return nil, nil, fmt.Errorf("failed to convert generated certificate to DER format: %s", cleanCertificateString)
}
certDERBytes = block.Bytes
}
certBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDERBytes,
})
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal the private key: %w", err)
}
keyBytes := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: privateKeyBytes,
})
cert, err := x509.ParseCertificate(certDERBytes)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse certificate: %w", err)
}
pubKeyDER, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal public key to DER format: %w", err)
}
if err := vtpm.ExtendPCR(vtpm.PCR15, pubKeyDER); err != nil {
return nil, nil, fmt.Errorf("failed to extend vTPM PCR with public key: %w", err)
}
return certBytes, keyBytes, nil
}
func processRequest(method, reqUrl string, data []byte, headers map[string]string, expectedRespCodes ...int) (http.Header, []byte, errors.SDKError) {
req, err := http.NewRequest(method, reqUrl, bytes.NewReader(data))
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
// Sets a default value for the Content-Type.
// Overridden if Content-Type is passed in the headers arguments.
req.Header.Add("Content-Type", "application/json")
for key, value := range headers {
req.Header.Add(key, value)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Do(req)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
defer resp.Body.Close()
sdkerr := errors.CheckError(resp, expectedRespCodes...)
if sdkerr != nil {
return make(http.Header), []byte{}, sdkerr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
return resp.Header, body, nil
}
+1 -23
View File
@@ -30,28 +30,6 @@ const bufSize = 1024 * 1024
var lis *bufconn.Listener
type DummyRWC struct{}
// Read fills p with byte(len(p)) and returns len(p).
func (l *DummyRWC) Read(p []byte) (int, error) {
n := len(p)
// Fill each byte in p with the value of n as a byte.
for i := range p {
p[i] = byte(n)
}
return n, nil
}
// Write simply returns len(p) indicating that all bytes were written.
func (l *DummyRWC) Write(p []byte) (int, error) {
// In this simple implementation, we ignore the data.
return len(p), nil
}
func (l *DummyRWC) Close() error {
return nil
}
func init() {
lis = bufconn.Listen(bufSize)
}
@@ -277,7 +255,7 @@ func (b *ThreadSafeBuffer) String() string {
}
func TestServerInitializationAndStartup(t *testing.T) {
vtpm.ExternalTPM = &DummyRWC{}
vtpm.ExternalTPM = &vtpm.DummyRWC{}
testCases := []struct {
name string
+1 -1
View File
@@ -23,7 +23,7 @@ type ServerConfiguration interface {
type BaseConfig struct {
Host string `env:"HOST" envDefault:"localhost"`
Port string `env:"PORT" envDefault:"7001"`
ServerCAFile string `env:"SERVER_CA_CERTS" envDefault:""`
ServerCAFile string `env:"SERVER_CA_CERTS" envDefault:""`
CertFile string `env:"SERVER_CERT" envDefault:""`
KeyFile string `env:"SERVER_KEY" envDefault:""`
ClientCAFile string `env:"CLIENT_CA_CERTS" envDefault:""`
+6 -22
View File
@@ -43,11 +43,11 @@ The service is configured using the environment variables from the following tab
| MANAGER_QEMU_VIRTIO_NET_PCI_ROMFILE | The file path for the ROM image for the virtio-net PCI device. | |
| MANAGER_QEMU_DISK_IMG_KERNEL_FILE | The file path for the kernel image. | img/bzImage |
| MANAGER_QEMU_DISK_IMG_ROOTFS_FILE | The file path for the root filesystem image. | img/rootfs.cpio.gz |
| MANAGER_QEMU_SEV_ID | The ID for the Secure Encrypted Virtualization (SEV) device. | sev0 |
| MANAGER_QEMU_SEV_CBITPOS | The position of the C-bit in the physical address. | 51 |
| MANAGER_QEMU_SEV_REDUCED_PHYS_BITS | The number of reduced physical address bits for SEV. | 1 |
| MANAGER_QEMU_ENABLE_HOST_DATA | Enable additional data for the SEV host. | false |
| MANAGER_QEMU_HOST_DATA | Additional data for the SEV host. | |
| MANAGER_QEMU_SEV_SNP_ID | The ID for the Secure Encrypted Virtualization (SEV-SNP) device. | sev0 |
| MANAGER_QEMU_SEV_SNP_CBITPOS | The position of the C-bit in the physical address. | 51 |
| MANAGER_QEMU_SEV_SNP_REDUCED_PHYS_BITS | The number of reduced physical address bits for SEV-SNP. | 1 |
| MANAGER_QEMU_ENABLE_HOST_DATA | Enable additional data for the SEV-SNP host. | false |
| MANAGER_QEMU_HOST_DATA | Additional data for the SEV-SNP host. | |
| MANAGER_QEMU_TDX_ID | The ID for the Trust Domain Extensions (TDX) device. | tdx0 |
| MANAGER_QEMU_QUOTE_GENERATION_PORT | The port number for virtual socket used to communicate with the Quote Generation Service (QGS). | 4050 |
| MANAGER_QEMU_OVMF_FILE | The file path for the OVMF file (combined OVMF_CODE and OVMF_VARS file). | /usr/share/ovmf/OVMF.fd |
@@ -58,7 +58,6 @@ The service is configured using the environment variables from the following tab
| MANAGER_QEMU_VSOCK_VNC | Whether to enable the virtual socket device for VNC. | 0 |
| MANAGER_QEMU_BIN_PATH | The file path for the QEMU binary. | qemu-system-x86_64 |
| MANAGER_QEMU_USE_SUDO | Whether to use sudo to run QEMU. | false |
| MANAGER_QEMU_ENABLE_SEV | Whether to enable Secure Encrypted Virtualization (SEV). | false |
| MANAGER_QEMU_ENABLE_SEV_SNP | Whether to enable Secure Nested Paging (SEV-SNP). | true |
| MANAGER_QEMU_ENABLE_TDX | Whether to enable Trust Domain Extensions (TDX). | false |
| MANAGER_QEMU_ENABLE_KVM | Whether to enable the Kernel-based Virtual Machine (KVM) acceleration. | true |
@@ -241,19 +240,6 @@ make manager
MANAGER_GRPC_URL=localhost:7001 \
MANAGER_LOG_LEVEL=debug \
MANAGER_QEMU_USE_SUDO=false \
MANAGER_QEMU_ENABLE_SEV=false \
./build/cocos-manager
```
To enable [AMD SEV](https://www.amd.com/en/developer/sev.html) support, start manager like this
```sh
MANAGER_GRPC_URL=localhost:7001
MANAGER_LOG_LEVEL=debug \
MANAGER_QEMU_USE_SUDO=true \
MANAGER_QEMU_ENABLE_SEV=true \
MANAGER_QEMU_SEV_CBITPOS=51 \
./build/cocos-manager
```
@@ -264,9 +250,8 @@ To enable [AMD SEV-SNP](https://www.amd.com/en/developer/sev.html) support, star
```sh
MANAGER_GRPC_URL=localhost:7001 \
MANAGER_LOG_LEVEL=debug \
MANAGER_QEMU_ENABLE_SEV=false \
MANAGER_QEMU_ENABLE_SEV_SNP=true \
MANAGER_QEMU_SEV_CBITPOS=51 \
MANAGER_QEMU_SEV_SNP_CBITPOS=51 \
MANAGER_QEMU_BIN_PATH=<path to QEMU binary> \
MANAGER_QEMU_IGVM_FILE=<path to IGVM file> \
./build/cocos-manager
@@ -277,7 +262,6 @@ To enable [TDX](https://www.intel.com/content/www/us/en/developer/tools/trust-do
```sh
MANAGER_GRPC_URL=localhost:7001 \
MANAGER_LOG_LEVEL=debug \
MANAGER_QEMU_ENABLE_SEV=false \
MANAGER_QEMU_ENABLE_SEV_SNP=false \
MANAGER_QEMU_ENABLE_TDX=true \
MANAGER_QEMU_CPU=host \
+425
View File
@@ -0,0 +1,425 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package grpc
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/ultravioletrs/cocos/manager"
"github.com/ultravioletrs/cocos/manager/mocks"
"google.golang.org/protobuf/types/known/emptypb"
)
func TestNewServer(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
assert.NotNil(t, server)
assert.IsType(t, &grpcServer{}, server)
grpcSrv := server.(*grpcServer)
assert.Equal(t, mockSvc, grpcSrv.svc)
}
func TestCreateVm(t *testing.T) {
tests := []struct {
name string
req *manager.CreateReq
mockPort string
mockId string
mockErr error
expectedRes *manager.CreateRes
expectedErr error
}{
{
name: "successful VM creation",
req: &manager.CreateReq{},
mockPort: "8080",
mockId: "vm-123",
mockErr: nil,
expectedRes: &manager.CreateRes{
ForwardedPort: "8080",
CvmId: "vm-123",
},
expectedErr: nil,
},
{
name: "VM creation with different port",
req: &manager.CreateReq{},
mockPort: "9090",
mockId: "vm-456",
mockErr: nil,
expectedRes: &manager.CreateRes{
ForwardedPort: "9090",
CvmId: "vm-456",
},
expectedErr: nil,
},
{
name: "VM creation failure",
req: &manager.CreateReq{},
mockPort: "",
mockId: "",
mockErr: errors.New("failed to create VM"),
expectedRes: nil,
expectedErr: errors.New("failed to create VM"),
},
{
name: "VM creation with empty request",
req: &manager.CreateReq{},
mockPort: "3000",
mockId: "vm-empty",
mockErr: nil,
expectedRes: &manager.CreateRes{
ForwardedPort: "3000",
CvmId: "vm-empty",
},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
mockSvc.On("CreateVM", mock.Anything, tt.req).Return(tt.mockPort, tt.mockId, tt.mockErr)
res, err := server.CreateVm(context.Background(), tt.req)
if tt.expectedErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr.Error(), err.Error())
assert.Nil(t, res)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedRes, res)
}
mockSvc.AssertExpectations(t)
})
}
}
func TestRemoveVm(t *testing.T) {
tests := []struct {
name string
req *manager.RemoveReq
mockErr error
expectedErr error
}{
{
name: "successful VM removal",
req: &manager.RemoveReq{
CvmId: "vm-123",
},
mockErr: nil,
expectedErr: nil,
},
{
name: "VM removal failure",
req: &manager.RemoveReq{
CvmId: "vm-456",
},
mockErr: errors.New("VM not found"),
expectedErr: errors.New("VM not found"),
},
{
name: "VM removal with empty ID",
req: &manager.RemoveReq{
CvmId: "",
},
mockErr: errors.New("invalid VM ID"),
expectedErr: errors.New("invalid VM ID"),
},
{
name: "VM removal with non-existent ID",
req: &manager.RemoveReq{
CvmId: "non-existent-vm",
},
mockErr: errors.New("VM does not exist"),
expectedErr: errors.New("VM does not exist"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
mockSvc.On("RemoveVM", mock.Anything, tt.req.CvmId).Return(tt.mockErr)
res, err := server.RemoveVm(context.Background(), tt.req)
if tt.expectedErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr.Error(), err.Error())
assert.Nil(t, res)
} else {
assert.NoError(t, err)
assert.Equal(t, &emptypb.Empty{}, res)
}
mockSvc.AssertExpectations(t)
})
}
}
func TestCVMInfo(t *testing.T) {
tests := []struct {
name string
req *manager.CVMInfoReq
mockOvmf string
mockCpuNum int
mockCpuType string
mockEosVersion string
expectedRes *manager.CVMInfoRes
}{
{
name: "successful CVM info retrieval",
req: &manager.CVMInfoReq{
Id: "cvm-123",
},
mockOvmf: "OVMF-v1.0",
mockCpuNum: 4,
mockCpuType: "Intel-x86_64",
mockEosVersion: "EOS-v2.1",
expectedRes: &manager.CVMInfoRes{
OvmfVersion: "OVMF-v1.0",
CpuNum: 4,
CpuType: "Intel-x86_64",
EosVersion: "EOS-v2.1",
Id: "cvm-123",
},
},
{
name: "CVM info with different values",
req: &manager.CVMInfoReq{
Id: "cvm-456",
},
mockOvmf: "OVMF-v2.0",
mockCpuNum: 8,
mockCpuType: "AMD-x86_64",
mockEosVersion: "EOS-v3.0",
expectedRes: &manager.CVMInfoRes{
OvmfVersion: "OVMF-v2.0",
CpuNum: 8,
CpuType: "AMD-x86_64",
EosVersion: "EOS-v3.0",
Id: "cvm-456",
},
},
{
name: "CVM info with empty ID",
req: &manager.CVMInfoReq{
Id: "",
},
mockOvmf: "OVMF-v1.5",
mockCpuNum: 2,
mockCpuType: "ARM64",
mockEosVersion: "EOS-v1.8",
expectedRes: &manager.CVMInfoRes{
OvmfVersion: "OVMF-v1.5",
CpuNum: 2,
CpuType: "ARM64",
EosVersion: "EOS-v1.8",
Id: "",
},
},
{
name: "CVM info with zero CPU count",
req: &manager.CVMInfoReq{
Id: "cvm-zero",
},
mockOvmf: "OVMF-v1.0",
mockCpuNum: 0,
mockCpuType: "Unknown",
mockEosVersion: "EOS-v1.0",
expectedRes: &manager.CVMInfoRes{
OvmfVersion: "OVMF-v1.0",
CpuNum: 0,
CpuType: "Unknown",
EosVersion: "EOS-v1.0",
Id: "cvm-zero",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
mockSvc.On("ReturnCVMInfo", mock.Anything).Return(
tt.mockOvmf, tt.mockCpuNum, tt.mockCpuType, tt.mockEosVersion)
res, err := server.CVMInfo(context.Background(), tt.req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedRes, res)
mockSvc.AssertExpectations(t)
})
}
}
func TestAttestationPolicy(t *testing.T) {
tests := []struct {
name string
req *manager.AttestationPolicyReq
mockPolicy string
mockErr error
expectedRes *manager.AttestationPolicyRes
expectedErr error
}{
{
name: "successful attestation policy fetch",
req: &manager.AttestationPolicyReq{
Id: "policy-123",
},
mockPolicy: `{"version": "1.0", "rules": ["rule1", "rule2"]}`,
mockErr: nil,
expectedRes: &manager.AttestationPolicyRes{
Info: []byte(`{"version": "1.0", "rules": ["rule1", "rule2"]}`),
Id: "policy-123",
},
expectedErr: nil,
},
{
name: "attestation policy fetch failure",
req: &manager.AttestationPolicyReq{
Id: "policy-456",
},
mockPolicy: "",
mockErr: errors.New("policy not found"),
expectedRes: nil,
expectedErr: errors.New("policy not found"),
},
{
name: "attestation policy with empty ID",
req: &manager.AttestationPolicyReq{
Id: "",
},
mockPolicy: "",
mockErr: errors.New("invalid policy ID"),
expectedRes: nil,
expectedErr: errors.New("invalid policy ID"),
},
{
name: "attestation policy with different content",
req: &manager.AttestationPolicyReq{
Id: "policy-789",
},
mockPolicy: `{"version": "2.0", "attestation_type": "SGX"}`,
mockErr: nil,
expectedRes: &manager.AttestationPolicyRes{
Info: []byte(`{"version": "2.0", "attestation_type": "SGX"}`),
Id: "policy-789",
},
expectedErr: nil,
},
{
name: "attestation policy with empty policy content",
req: &manager.AttestationPolicyReq{
Id: "policy-empty",
},
mockPolicy: "",
mockErr: nil,
expectedRes: &manager.AttestationPolicyRes{
Info: []byte{},
Id: "policy-empty",
},
expectedErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
mockSvc.On("FetchAttestationPolicy", mock.Anything, tt.req.Id).Return([]byte(tt.mockPolicy), tt.mockErr)
res, err := server.AttestationPolicy(context.Background(), tt.req)
if tt.expectedErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.expectedErr.Error(), err.Error())
assert.Nil(t, res)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedRes, res)
}
mockSvc.AssertExpectations(t)
})
}
}
func TestContextCancellation(t *testing.T) {
t.Run("CreateVm with cancelled context", func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel the context immediately
req := &manager.CreateReq{}
mockSvc.On("CreateVM", mock.Anything, req).Return("", "", context.Canceled)
res, err := server.CreateVm(ctx, req)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.Nil(t, res)
mockSvc.AssertExpectations(t)
})
t.Run("RemoveVm with cancelled context", func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel the context immediately
req := &manager.RemoveReq{CvmId: "vm-123"}
mockSvc.On("RemoveVM", mock.Anything, "vm-123").Return(context.Canceled)
res, err := server.RemoveVm(ctx, req)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
assert.Nil(t, res)
mockSvc.AssertExpectations(t)
})
}
func TestErrorHandling(t *testing.T) {
t.Run("service returns multiple error types", func(t *testing.T) {
mockSvc := new(mocks.Service)
server := NewServer(mockSvc)
// Test with different error types
customErr := errors.New("custom service error")
req := &manager.CreateReq{}
mockSvc.On("CreateVM", mock.Anything, req).Return("", "", customErr)
res, err := server.CreateVm(context.Background(), req)
assert.Error(t, err)
assert.Equal(t, customErr, err)
assert.Nil(t, res)
mockSvc.AssertExpectations(t)
})
}
+17
View File
@@ -75,3 +75,20 @@ func (lm *loggingMiddleware) ReturnCVMInfo(ctx context.Context) (string, int, st
return lm.svc.ReturnCVMInfo(ctx)
}
func (lm *loggingMiddleware) Shutdown() (err error) {
defer func(begin time.Time) {
if err != nil {
lm.logger.Warn("Method Shutdown completed with error",
"time_taken", time.Since(begin),
"error", err,
)
return
}
lm.logger.Info("Method Shutdown completed successfully",
"time_taken", time.Since(begin),
)
}(time.Now())
return lm.svc.Shutdown()
}
+9
View File
@@ -67,3 +67,12 @@ func (ms *metricsMiddleware) ReturnCVMInfo(ctx context.Context) (string, int, st
return ms.svc.ReturnCVMInfo(ctx)
}
func (ms *metricsMiddleware) Shutdown() error {
defer func(begin time.Time) {
ms.counter.With("method", "Shutdown").Add(1)
ms.latency.With("method", "Shutdown").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.Shutdown()
}
+72 -29
View File
@@ -12,23 +12,71 @@ import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/google/go-sev-guest/proto/check"
"github.com/ultravioletrs/cocos/manager/qemu"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/cmdconfig"
"github.com/virtee/sev-snp-measure-go/cpuid"
"github.com/virtee/sev-snp-measure-go/guest"
"github.com/virtee/sev-snp-measure-go/vmmtypes"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
const defGuestFeatures = 0x1
func (ms *managerService) FetchAttestationPolicy(_ context.Context, computationId string) ([]byte, error) {
ms.mu.Lock()
vm, exists := ms.vms[computationId]
ms.mu.Unlock()
if !exists {
return nil, fmt.Errorf("computationId %s not found", computationId)
}
vmi, ok := vm.GetConfig().(qemu.VMInfo)
if !ok {
return nil, fmt.Errorf("failed to cast config to qemu.VMInfo")
}
var attestPolicyCmd *cmdconfig.CmdConfig
var err error
switch {
case vmi.Config.EnableSEVSNP:
attestPolicyCmd, err = fetchSNPAttestationPolicy(ms)
case vmi.Config.EnableTDX:
attestPolicyCmd, err = fetchTDXAttestationPolicy(ms)
}
if err != nil {
return nil, err
}
var stdOutByte []byte
ms.ap.Lock()
switch {
case vmi.Config.EnableSEVSNP:
stdOutByte, err = attestPolicyCmd.Run(ms.attestationPolicyBinaryPath)
case vmi.Config.EnableTDX:
stdOutByte, err = attestPolicyCmd.Run("")
}
ms.ap.Unlock()
if err != nil {
return nil, err
}
var policy []byte
switch {
case vmi.Config.EnableSEVSNP:
policy, err = readSEVSNPPolicy(stdOutByte, ms, vmi)
case vmi.Config.EnableTDX:
policy = stdOutByte
err = nil
}
if err != nil {
return nil, err
}
return policy, nil
}
func fetchSNPAttestationPolicy(ms *managerService) (*cmdconfig.CmdConfig, error) {
var stderrBuffer bytes.Buffer
options := []string{"--policy", "196608"}
@@ -44,38 +92,33 @@ func (ms *managerService) FetchAttestationPolicy(_ context.Context, computationI
return nil, err
}
ms.mu.Lock()
vm, exists := ms.vms[computationId]
ms.mu.Unlock()
if !exists {
return nil, fmt.Errorf("computationId %s not found", computationId)
}
return attestPolicyCmd, nil
}
vmi, ok := vm.GetConfig().(qemu.VMInfo)
if !ok {
return nil, fmt.Errorf("failed to cast config to qemu.VMInfo")
}
func fetchTDXAttestationPolicy(ms *managerService) (*cmdconfig.CmdConfig, error) {
var stderrBuffer bytes.Buffer
ms.ap.Lock()
stdOutByte, err := attestPolicyCmd.Run(ms.attestationPolicyBinaryPath)
ms.ap.Unlock()
stderr := bufio.NewWriter(&stderrBuffer)
attestPolicyCmd, err := cmdconfig.NewCmdConfig(ms.attestationPolicyBinaryPath, nil, stderr)
if err != nil {
return nil, err
}
return attestPolicyCmd, nil
}
func readSEVSNPPolicy(stdOutByte []byte, ms *managerService, vmi qemu.VMInfo) ([]byte, error) {
attestationPolicy := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
if err = attestation.ReadAttestationPolicyFromByte(stdOutByte, &attestationPolicy); err != nil {
if err := vtpm.ReadPolicyFromByte(stdOutByte, &attestationPolicy); err != nil {
return nil, err
}
var stderrBuffer bytes.Buffer
var measurement []byte
var err error
switch {
case vmi.Config.EnableSEV:
measurement, err = guest.CalcLaunchDigest(guest.SEV, vmi.Config.SMPCount, uint64(cpuid.CpuSigs[ms.qemuCfg.CPU]), vmi.Config.OVMFCodeConfig.File, vmi.Config.KernelFile, vmi.Config.RootFsFile, strconv.Quote(qemu.KernelCommandLine), defGuestFeatures, "", vmmtypes.QEMU, false, "", 0)
if err != nil {
return nil, err
}
case vmi.Config.EnableSEVSNP:
stderr := bufio.NewWriter(&stderrBuffer)
options := cmdconfig.IgvmMeasureOptions
@@ -110,8 +153,8 @@ func (ms *managerService) FetchAttestationPolicy(_ context.Context, computationI
attestationPolicy.Config.Policy.Measurement = measurement
}
if vmi.Config.SEVConfig.EnableHostData {
hostData, err := base64.StdEncoding.DecodeString(vmi.Config.SEVConfig.HostData)
if vmi.Config.SEVSNPConfig.EnableHostData {
hostData, err := base64.StdEncoding.DecodeString(vmi.Config.SEVSNPConfig.HostData)
if err != nil {
return nil, err
}
@@ -120,7 +163,7 @@ func (ms *managerService) FetchAttestationPolicy(_ context.Context, computationI
attestationPolicy.Config.Policy.MinimumLaunchTcb = vmi.LaunchTCB
f, err := json.MarshalIndent(attestationPolicy, "", " ")
f, err := vtpm.ConvertPolicyToJSON(&attestationPolicy)
if err != nil {
return nil, err
}
+6 -10
View File
@@ -53,22 +53,18 @@ func TestFetchAttestationPolicy(t *testing.T) {
expectedResult map[string]interface{}
}{
{
name: "Valid SEV configuration",
computationId: "sev-computation",
name: "Valid SEV-SNP configuration",
computationId: "sev-snp-computation",
binaryBehavior: "success",
vmConfig: qemu.VMInfo{
Config: qemu.Config{
EnableSEV: true,
EnableSEVSNP: false,
EnableSEVSNP: true,
SMPCount: 2,
CPU: "EPYC",
OVMFCodeConfig: qemu.OVMFCodeConfig{
File: "/path/to/OVMF_CODE.fd",
},
},
LaunchTCB: 0,
},
expectedError: "open /path/to/OVMF_CODE.fd: no such file or directory",
expectedError: "pathToBinary cannot be empty",
},
{
name: "Invalid computation ID",
@@ -90,7 +86,7 @@ func TestFetchAttestationPolicy(t *testing.T) {
binaryBehavior: "fail",
vmConfig: qemu.VMInfo{
Config: qemu.Config{
EnableSEV: true,
EnableSEVSNP: true,
},
LaunchTCB: 0,
},
@@ -102,7 +98,7 @@ func TestFetchAttestationPolicy(t *testing.T) {
binaryBehavior: "no_json",
vmConfig: qemu.VMInfo{
Config: qemu.Config{
EnableSEV: true,
EnableSEVSNP: true,
},
LaunchTCB: 0,
},
+45
View File
@@ -265,6 +265,51 @@ func (_c *Service_ReturnCVMInfo_Call) RunAndReturn(run func(context.Context) (st
return _c
}
// Shutdown provides a mock function with no fields
func (_m *Service) Shutdown() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Shutdown")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Service_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown'
type Service_Shutdown_Call struct {
*mock.Call
}
// Shutdown is a helper method to define mock.On call
func (_e *Service_Expecter) Shutdown() *Service_Shutdown_Call {
return &Service_Shutdown_Call{Call: _e.mock.On("Shutdown")}
}
func (_c *Service_Shutdown_Call) Run(run func()) *Service_Shutdown_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Service_Shutdown_Call) Return(_a0 error) *Service_Shutdown_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Service_Shutdown_Call) RunAndReturn(run func() error) *Service_Shutdown_Call {
_c.Call.Return(run)
return _c
}
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewService(t interface {
+18 -24
View File
@@ -12,7 +12,7 @@ import (
const (
BaseGuestCID = 3
KernelCommandLine = "quiet console=null"
TdxObject = "'{\"qom-type\":\"tdx-guest\",\"id\":\"%s\",\"quote-generation-socket\":{\"type\": \"vsock\", \"cid\":\"2\",\"port\":\"%d\"}}'"
TDXObject = "{\"qom-type\":\"tdx-guest\",\"id\":\"%s\",\"quote-generation-socket\":{\"type\": \"vsock\", \"cid\":\"2\",\"port\":\"%d\"}}"
)
type MemoryConfig struct {
@@ -55,10 +55,10 @@ type DiskImgConfig struct {
RootFsFile string `env:"DISK_IMG_ROOTFS_FILE" envDefault:"img/rootfs.cpio.gz"`
}
type SEVConfig struct {
ID string `env:"SEV_ID" envDefault:"sev0"`
CBitPos int `env:"SEV_CBITPOS" envDefault:"51"`
ReducedPhysBits int `env:"SEV_REDUCED_PHYS_BITS" envDefault:"1"`
type SEVSNPConfig struct {
ID string `env:"SEV_SNP_ID" envDefault:"sev0"`
CBitPos int `env:"SEV_SNP_CBIT_POS" envDefault:"51"`
ReducedPhysBits int `env:"SEV_SNP_REDUCED_PHYS_BITS" envDefault:"1"`
EnableHostData bool `env:"ENABLE_HOST_DATA" envDefault:"false"`
HostData string `env:"HOST_DATA" envDefault:""`
}
@@ -80,7 +80,6 @@ type VSockConfig struct {
}
type Config struct {
EnableSEV bool
EnableSEVSNP bool
EnableTDX bool
QemuBinPath string `env:"BIN_PATH" envDefault:"qemu-system-x86_64"`
@@ -110,8 +109,8 @@ type Config struct {
// disk
DiskImgConfig
// SEV
SEVConfig
// SEV-SNP
SEVSNPConfig
// TDX
TDXConfig
@@ -189,23 +188,19 @@ func (config Config) ConstructQemuArgs() []string {
args = append(args, "-device", fmt.Sprintf("vhost-vsock-pci,id=%s,guest-cid=%d", config.VSockConfig.ID, config.VSockConfig.GuestCID))
// SEV
if config.EnableSEV || config.EnableSEVSNP {
sevType := "sev-guest"
// SEV-SNP
if config.EnableSEVSNP {
sevSnpType := "sev-snp-guest"
hostData := ""
args = append(args, "-machine",
fmt.Sprintf("confidential-guest-support=%s,memory-backend=%s,igvm-cfg=%s",
config.SEVConfig.ID,
config.SEVSNPConfig.ID,
config.MemID,
config.IGVMConfig.ID))
if config.EnableSEVSNP {
sevType = "sev-snp-guest"
if config.SEVConfig.EnableHostData {
hostData = fmt.Sprintf(",host-data=%s", config.SEVConfig.HostData)
}
if config.SEVSNPConfig.EnableHostData {
hostData = fmt.Sprintf(",host-data=%s", config.SEVSNPConfig.HostData)
}
args = append(args, "-object",
@@ -215,10 +210,10 @@ func (config Config) ConstructQemuArgs() []string {
args = append(args, "-object",
fmt.Sprintf("%s,id=%s,cbitpos=%d,reduced-phys-bits=%d%s",
sevType,
config.SEVConfig.ID,
config.SEVConfig.CBitPos,
config.SEVConfig.ReducedPhysBits,
sevSnpType,
config.SEVSNPConfig.ID,
config.SEVSNPConfig.CBitPos,
config.SEVSNPConfig.ReducedPhysBits,
hostData))
args = append(args, "-object",
@@ -229,7 +224,7 @@ func (config Config) ConstructQemuArgs() []string {
if config.EnableTDX {
args = append(args, "-object",
fmt.Sprintf(TdxObject,
fmt.Sprintf(TDXObject,
config.TDXConfig.ID,
config.TDXConfig.QuoteGenerationPort))
@@ -278,7 +273,6 @@ func NewConfig() (*Config, error) {
return nil, err
}
cfg.EnableSEV = SEVEnabledOnHost()
cfg.EnableSEVSNP = SEVSNPEnabledOnHost()
cfg.EnableTDX = TDXEnabledOnHost()
+2 -2
View File
@@ -127,7 +127,7 @@ func TestConstructQemuArgs(t *testing.T) {
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
SEVConfig: SEVConfig{
SEVSNPConfig: SEVSNPConfig{
ID: "sev0",
CBitPos: 51,
ReducedPhysBits: 1,
@@ -174,7 +174,7 @@ func TestConstructQemuArgs(t *testing.T) {
func TestConstructQemuArgs_HostData(t *testing.T) {
config := Config{
EnableSEVSNP: true,
SEVConfig: SEVConfig{
SEVSNPConfig: SEVSNPConfig{
ID: "sev0",
CBitPos: 51,
ReducedPhysBits: 1,
+4 -19
View File
@@ -61,10 +61,10 @@ func (v *qemuVM) Start() (err error) {
}
v.vmi.Config.NetDevConfig.ID = fmt.Sprintf("%s-%s", v.vmi.Config.NetDevConfig.ID, id)
v.vmi.Config.SEVConfig.ID = fmt.Sprintf("%s-%s", v.vmi.Config.SEVConfig.ID, id)
v.vmi.Config.SEVSNPConfig.ID = fmt.Sprintf("%s-%s", v.vmi.Config.SEVSNPConfig.ID, id)
v.vmi.Config.TDXConfig.ID = fmt.Sprintf("%s-%s", v.vmi.Config.TDXConfig.ID, id)
if !v.vmi.Config.EnableSEVSNP {
if !v.vmi.Config.EnableSEVSNP && !v.vmi.Config.EnableTDX {
// Copy firmware vars file.
srcFile := v.vmi.Config.OVMFVarsConfig.File
dstFile := fmt.Sprintf("%s/%s-%s.fd", tmpDir, firmwareVars, id)
@@ -200,27 +200,12 @@ func (v *qemuVM) GetConfig() interface{} {
return v.vmi
}
func SEVEnabled(cpuinfo string, sevPresent bool) bool {
return strings.Contains(cpuinfo, "sev") && sevPresent
}
func SEVSNPEnabled(cpuinfo, kernelParam string) bool {
return strings.Contains(cpuinfo, "sev_snp") && strings.TrimSpace(kernelParam) == "1"
return strings.Contains(cpuinfo, "sev_snp") && strings.TrimSpace(kernelParam) == "Y"
}
func TDXEnabled(cpuinfo, kernelParam string) bool {
return strings.Contains(cpuinfo, "tdx_host_platform") && strings.TrimSpace(kernelParam) == "1"
}
// Checks if SEV is supported and usable by verifying both CPU flags and the /dev/sev device.
func SEVEnabledOnHost() bool {
cpuinfo, err := os.ReadFile("/proc/cpuinfo")
if err != nil {
return false
}
_, err = os.Stat("/dev/sev")
return SEVEnabled(string(cpuinfo), err == nil)
return strings.Contains(cpuinfo, "tdx_host_platform") && strings.TrimSpace(kernelParam) == "Y"
}
func SEVSNPEnabledOnHost() bool {
+10 -16
View File
@@ -162,23 +162,9 @@ func TestGetConfig(t *testing.T) {
assert.Equal(t, expectedConfig, config)
}
func TestSEVEnabled(t *testing.T) {
t.Run("cpuinfo contains sev and device exists", func(t *testing.T) {
assert.True(t, SEVEnabled("flags: xyz sev abc", true))
})
t.Run("cpuinfo missing sev", func(t *testing.T) {
assert.False(t, SEVEnabled("flags: xyz abc", true))
})
t.Run("device does not exist", func(t *testing.T) {
assert.False(t, SEVEnabled("flags: sev abc", false))
})
}
func TestSEVSNPEnabled(t *testing.T) {
t.Run("cpuinfo and kvm param correct", func(t *testing.T) {
assert.True(t, SEVSNPEnabled("flags: sev_snp abc", "1"))
assert.True(t, SEVSNPEnabled("flags: sev_snp abc", "Y"))
})
t.Run("missing sev_snp in cpuinfo", func(t *testing.T) {
@@ -192,7 +178,7 @@ func TestSEVSNPEnabled(t *testing.T) {
func TestTDXEnabled(t *testing.T) {
t.Run("cpuinfo and kvm param correct", func(t *testing.T) {
assert.True(t, TDXEnabled("flags: tdx_host_platform abc", "1"))
assert.True(t, TDXEnabled("flags: tdx_host_platform abc", "Y"))
})
t.Run("missing tdx_host_platform in cpuinfo", func(t *testing.T) {
@@ -203,3 +189,11 @@ func TestTDXEnabled(t *testing.T) {
assert.False(t, TDXEnabled("flags: tdx_host_platform", "0"))
})
}
func TestSEVSNPEnabledOnHost(t *testing.T) {
assert.False(t, SEVSNPEnabledOnHost())
}
func TestTDXEnabledOnHost(t *testing.T) {
assert.False(t, TDXEnabledOnHost())
}
-28
View File
@@ -1,28 +0,0 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package qemu
import (
"encoding/json"
"github.com/mdlayher/vsock"
"github.com/ultravioletrs/cocos/agent"
)
const VsockConfigPort uint32 = 9999
func (v *qemuVM) SendAgentConfig(ac agent.Computation) error {
conn, err := vsock.Dial(uint32(v.vmi.Config.GuestCID), VsockConfigPort, nil)
if err != nil {
return err
}
defer conn.Close()
payload, err := json.Marshal(ac)
if err != nil {
return err
}
if _, err := conn.Write(payload); err != nil {
return err
}
return nil
}
+35 -30
View File
@@ -3,8 +3,6 @@
package manager
import (
"bufio"
"bytes"
"context"
"encoding/base64"
"fmt"
@@ -23,7 +21,7 @@ import (
"github.com/ultravioletrs/cocos/manager/qemu"
"github.com/ultravioletrs/cocos/manager/vm"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/cmdconfig"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/ultravioletrs/cocos/pkg/manager"
"golang.org/x/crypto/sha3"
)
@@ -83,6 +81,8 @@ type Service interface {
FetchAttestationPolicy(ctx context.Context, computationID string) ([]byte, error)
// ReturnCVMInfo returns CVM information needed for attestation verification and validation.
ReturnCVMInfo(ctx context.Context) (string, int, string, string)
// Shutdown gracefully shuts down the service
Shutdown() error
}
type managerService struct {
@@ -99,13 +99,13 @@ type managerService struct {
portRangeMax int
persistence qemu.Persistence
eosVersion string
ctx context.Context
ttlManager *TTLManager
}
var _ Service = (*managerService)(nil)
// New instantiates the manager service implementation.
func New(ctx context.Context, cfg qemu.Config, attestationPolicyBinPath string, igvmMeasurementBinaryPath string, pcrValuesFilePath string, logger *slog.Logger, vmFactory vm.Provider, eosVersion string) (Service, error) {
func New(cfg qemu.Config, attestationPolicyBinPath string, igvmMeasurementBinaryPath string, pcrValuesFilePath string, logger *slog.Logger, vmFactory vm.Provider, eosVersion string) (Service, error) {
start, end, err := decodeRange(cfg.HostFwdRange)
if err != nil {
return nil, err
@@ -128,7 +128,7 @@ func New(ctx context.Context, cfg qemu.Config, attestationPolicyBinPath string,
portRangeMax: end,
persistence: persistence,
eosVersion: eosVersion,
ctx: ctx,
ttlManager: NewTTLManager(),
}
if err := ms.restoreVMs(); err != nil {
@@ -160,24 +160,15 @@ func (ms *managerService) CreateVM(ctx context.Context, req *CreateReq) (string,
cfg.Config.CertsMount = tmpCertsDir
cfg.Config.EnvMount = tmpEnvDir
if ms.qemuCfg.EnableSEVSNP || ms.qemuCfg.EnableSEV {
var stderrBuffer bytes.Buffer
options := []string{"--policy", "196608"}
if ms.pcrValuesFilePath != "" {
pcrValues := []string{"--pcr", ms.pcrValuesFilePath}
options = append(options, pcrValues...)
}
stderr := bufio.NewWriter(&stderrBuffer)
attestPolicyCmd, err := cmdconfig.NewCmdConfig("sudo", options, stderr)
if ms.qemuCfg.EnableSEVSNP {
attestPolicyCmd, err := fetchSNPAttestationPolicy(ms)
if err != nil {
return "", id, err
}
var stdOutByte []byte
ms.ap.Lock()
stdOutByte, err := attestPolicyCmd.Run(ms.attestationPolicyBinaryPath)
stdOutByte, err = attestPolicyCmd.Run(ms.attestationPolicyBinaryPath)
ms.ap.Unlock()
if err != nil {
return "", id, errors.Wrap(ErrFailedToCreateAttestationPolicy, err)
@@ -185,7 +176,7 @@ func (ms *managerService) CreateVM(ctx context.Context, req *CreateReq) (string,
attestationPolicy := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
if err = attestation.ReadAttestationPolicyFromByte(stdOutByte, &attestationPolicy); err != nil {
if err = vtpm.ReadPolicyFromByte(stdOutByte, &attestationPolicy); err != nil {
return "", id, errors.Wrap(ErrUnmarshalFailed, err)
}
@@ -218,7 +209,7 @@ func (ms *managerService) CreateVM(ctx context.Context, req *CreateReq) (string,
if cfg.Config.EnableSEVSNP {
todo := sha3.Sum256([]byte("TODO"))
// Define host-data value of QEMU for SEV-SNP, with a base64 encoding of the computation hash.
cfg.Config.SEVConfig.HostData = base64.StdEncoding.EncodeToString(todo[:])
cfg.Config.SEVSNPConfig.HostData = base64.StdEncoding.EncodeToString(todo[:])
}
cvm := ms.vmFactory(cfg, id, ms.logger)
@@ -235,16 +226,13 @@ func (ms *managerService) CreateVM(ctx context.Context, req *CreateReq) (string,
return "", id, err
}
go func() {
select {
case <-time.After(ttl):
if err := ms.RemoveVM(ctx, id); err != nil {
ms.logger.Error("Failed to remove VM after TTL", "error", err)
}
case <-ms.ctx.Done():
return
ms.ttlManager.SetTTL(id, ttl, func() { //nolint:contextcheck
if err := ms.RemoveVM(context.Background(), id); err != nil {
ms.logger.Error("Failed to remove VM after TTL expiry", "vmID", id, "error", err)
} else {
ms.logger.Info("Successfully removed VM after TTL expiry", "vmID", id)
}
}()
})
}
pid := cvm.GetProcess()
@@ -270,6 +258,9 @@ func (ms *managerService) CreateVM(ctx context.Context, req *CreateReq) (string,
func (ms *managerService) RemoveVM(ctx context.Context, computationID string) error {
ms.mu.Lock()
defer ms.mu.Unlock()
ms.ttlManager.CancelTTL(computationID)
cvm, ok := ms.vms[computationID]
if !ok {
return ErrNotFound
@@ -290,6 +281,20 @@ func (ms *managerService) ReturnCVMInfo(ctx context.Context) (string, int, strin
return ms.qemuCfg.OVMFCodeConfig.Version, ms.qemuCfg.SMPCount, ms.qemuCfg.CPU, ms.eosVersion
}
// Shutdown gracefully shuts down the service.
func (ms *managerService) Shutdown() error {
ms.logger.Info("Shutting down manager service")
ms.ttlManager.CancelAll()
ms.mu.Lock()
defer ms.mu.Unlock()
ms.vms = make(map[string]vm.VM)
return nil
}
func getFreePort(minPort, maxPort int) (int, error) {
if checkPortisFree(minPort) {
return minPort, nil
+32 -2
View File
@@ -30,7 +30,7 @@ func TestNew(t *testing.T) {
logger := slog.Default()
vmf := new(mocks.Provider)
service, err := New(context.Background(), cfg, "", "", "", logger, vmf.Execute, "")
service, err := New(cfg, "", "", "", logger, vmf.Execute, "")
require.NoError(t, err)
assert.NotNil(t, service)
@@ -47,24 +47,35 @@ func TestRun(t *testing.T) {
binaryBehavior string
vmStartError error
expectedError error
ttl string
}{
{
name: "Successful run",
binaryBehavior: "success",
vmStartError: nil,
expectedError: nil,
ttl: "",
},
{
name: "VM start failure",
binaryBehavior: "success",
vmStartError: assert.AnError,
expectedError: assert.AnError,
ttl: "",
},
{
name: "Invalid attestation policy",
binaryBehavior: "fail",
vmStartError: nil,
expectedError: ErrFailedToCreateAttestationPolicy,
ttl: "",
},
{
name: "With TTL",
binaryBehavior: "success",
vmStartError: nil,
expectedError: nil,
ttl: "10s",
},
}
@@ -101,11 +112,12 @@ func TestRun(t *testing.T) {
vms: make(map[string]vm.VM),
vmFactory: vmf.Execute,
persistence: persistence,
ttlManager: NewTTLManager(),
}
ctx := context.Background()
port, _, err := ms.CreateVM(ctx, &CreateReq{})
port, _, err := ms.CreateVM(ctx, &CreateReq{Ttl: tt.ttl})
if tt.expectedError != nil {
assert.Error(t, err)
@@ -165,6 +177,7 @@ func TestStop(t *testing.T) {
logger: logger,
vms: make(map[string]vm.VM),
persistence: persistence,
ttlManager: NewTTLManager(),
}
vmMock := new(mocks.VM)
@@ -287,3 +300,20 @@ func TestProcessExists(t *testing.T) {
assert.False(t, ms.processExists(1)) // PID 1 is usually the init process.
}
}
func TestShutdown(t *testing.T) {
ms := &managerService{
vms: make(map[string]vm.VM),
ttlManager: NewTTLManager(),
logger: mglog.NewMock(),
}
vmMock := new(mocks.VM)
vmMock.On("Stop").Return(nil).Once()
ms.vms["test-vm"] = vmMock
err := ms.Shutdown()
assert.NoError(t, err)
assert.Len(t, ms.vms, 0)
}
+7
View File
@@ -48,3 +48,10 @@ func (tm *tracingMiddleware) ReturnCVMInfo(ctx context.Context) (string, int, st
return tm.svc.ReturnCVMInfo(ctx)
}
func (tm *tracingMiddleware) Shutdown() error {
_, span := tm.tracer.Start(context.Background(), "shutdown")
defer span.End()
return tm.svc.Shutdown()
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package manager
import (
"context"
sync "sync"
"time"
)
// TTLManager handles TTL functionality for VMs.
type TTLManager struct {
timers map[string]*time.Timer
mu sync.RWMutex
}
// NewTTLManager creates a new TTL manager.
func NewTTLManager() *TTLManager {
return &TTLManager{
timers: make(map[string]*time.Timer),
}
}
// SetTTL sets a TTL for a VM and returns a function to cancel it.
func (tm *TTLManager) SetTTL(vmID string, ttl time.Duration, onExpiry func()) context.CancelFunc {
tm.mu.Lock()
defer tm.mu.Unlock()
if timer, exists := tm.timers[vmID]; exists {
timer.Stop()
}
timer := time.AfterFunc(ttl, onExpiry)
tm.timers[vmID] = timer
return func() {
tm.mu.Lock()
defer tm.mu.Unlock()
if t, exists := tm.timers[vmID]; exists {
t.Stop()
delete(tm.timers, vmID)
}
}
}
// CancelTTL cancels the TTL for a specific VM.
func (tm *TTLManager) CancelTTL(vmID string) {
tm.mu.Lock()
defer tm.mu.Unlock()
if timer, exists := tm.timers[vmID]; exists {
timer.Stop()
delete(tm.timers, vmID)
}
}
// CancelAll cancels all active TTLs.
func (tm *TTLManager) CancelAll() {
tm.mu.Lock()
defer tm.mu.Unlock()
for vmID, timer := range tm.timers {
timer.Stop()
delete(tm.timers, vmID)
}
}
+342
View File
@@ -0,0 +1,342 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package manager
import (
"fmt"
"sync"
"testing"
"time"
)
func TestNewTTLManager(t *testing.T) {
tm := NewTTLManager()
if tm == nil {
t.Fatal("NewTTLManager() returned nil")
}
if tm.timers == nil {
t.Fatal("NewTTLManager() did not initialize timers map")
}
if len(tm.timers) != 0 {
t.Errorf("NewTTLManager() timers map should be empty, got %d entries", len(tm.timers))
}
}
func TestSetTTL_Basic(t *testing.T) {
tm := NewTTLManager()
mu := sync.Mutex{}
expired := false
vmID := "test-vm-1"
ttl := 50 * time.Millisecond
cancelFunc := tm.SetTTL(vmID, ttl, func() {
mu.Lock()
defer mu.Unlock()
expired = true
})
tm.mu.RLock()
if _, exists := tm.timers[vmID]; !exists {
t.Error("Timer was not created for VM")
}
tm.mu.RUnlock()
time.Sleep(100 * time.Millisecond)
mu.Lock()
if !expired {
t.Error("TTL did not expire as expected")
}
mu.Unlock()
cancelFunc()
tm.mu.RLock()
if _, exists := tm.timers[vmID]; exists {
t.Error("Timer should be cleaned up after expiry")
}
tm.mu.RUnlock()
}
func TestSetTTL_CancelBeforeExpiry(t *testing.T) {
tm := NewTTLManager()
expired := false
vmID := "test-vm-2"
ttl := 100 * time.Millisecond
cancelFunc := tm.SetTTL(vmID, ttl, func() {
expired = true
})
time.Sleep(20 * time.Millisecond)
cancelFunc()
time.Sleep(150 * time.Millisecond)
if expired {
t.Error("TTL should not have expired after being cancelled")
}
tm.mu.RLock()
if _, exists := tm.timers[vmID]; exists {
t.Error("Timer should be cleaned up after cancellation")
}
tm.mu.RUnlock()
}
func TestSetTTL_OverwriteExistingTimer(t *testing.T) {
tm := NewTTLManager()
mu := sync.Mutex{}
firstExpired := false
secondExpired := false
vmID := "test-vm-3"
// Set first TTL
tm.SetTTL(vmID, 200*time.Millisecond, func() {
firstExpired = true
})
// Immediately overwrite with second TTL
tm.SetTTL(vmID, 50*time.Millisecond, func() {
mu.Lock()
defer mu.Unlock()
secondExpired = true
})
// Wait for second TTL to expire
time.Sleep(100 * time.Millisecond)
if firstExpired {
t.Error("First TTL should not have expired (it was overwritten)")
}
mu.Lock()
if !secondExpired {
t.Error("Second TTL should have expired")
}
mu.Unlock()
// Verify only one timer entry exists (or none after cleanup)
tm.mu.RLock()
count := len(tm.timers)
tm.mu.RUnlock()
if count > 1 {
t.Errorf("Expected at most 1 timer entry, got %d", count)
}
}
func TestSetTTL_MultipleConcurrentTimers(t *testing.T) {
tm := NewTTLManager()
numVMs := 5
expiredCount := int32(0)
var mu sync.Mutex
for i := 0; i < numVMs; i++ {
vmID := fmt.Sprintf("vm-%d", i)
tm.SetTTL(vmID, 50*time.Millisecond, func() {
mu.Lock()
expiredCount++
mu.Unlock()
})
}
tm.mu.RLock()
if len(tm.timers) != numVMs {
t.Errorf("Expected %d timers, got %d", numVMs, len(tm.timers))
}
tm.mu.RUnlock()
time.Sleep(100 * time.Millisecond)
mu.Lock()
finalCount := expiredCount
mu.Unlock()
if int(finalCount) != numVMs {
t.Errorf("Expected %d timers to expire, got %d", numVMs, finalCount)
}
}
func TestCancelTTL_ExistingTimer(t *testing.T) {
tm := NewTTLManager()
expired := false
vmID := "test-vm-4"
tm.SetTTL(vmID, 100*time.Millisecond, func() {
expired = true
})
// Cancel the timer
tm.CancelTTL(vmID)
time.Sleep(150 * time.Millisecond)
if expired {
t.Error("TTL should not have expired after being cancelled")
}
// Verify timer was removed
tm.mu.RLock()
if _, exists := tm.timers[vmID]; exists {
t.Error("Timer should be removed after cancellation")
}
tm.mu.RUnlock()
}
func TestCancelTTL_NonExistentTimer(t *testing.T) {
tm := NewTTLManager()
// Should not panic when cancelling non-existent timer
tm.CancelTTL("non-existent-vm")
// Verify timers map is still empty
tm.mu.RLock()
if len(tm.timers) != 0 {
t.Errorf("Expected empty timers map, got %d entries", len(tm.timers))
}
tm.mu.RUnlock()
}
func TestCancelAll_MultipleTimers(t *testing.T) {
tm := NewTTLManager()
numVMs := 3
expiredCount := int32(0)
var mu sync.Mutex
for i := 0; i < numVMs; i++ {
vmID := fmt.Sprintf("vm-%d", i)
tm.SetTTL(vmID, 200*time.Millisecond, func() {
mu.Lock()
expiredCount++
mu.Unlock()
})
}
tm.mu.RLock()
if len(tm.timers) != numVMs {
t.Errorf("Expected %d timers, got %d", numVMs, len(tm.timers))
}
tm.mu.RUnlock()
tm.CancelAll()
tm.mu.RLock()
if len(tm.timers) != 0 {
t.Errorf("Expected 0 timers after CancelAll, got %d", len(tm.timers))
}
tm.mu.RUnlock()
time.Sleep(250 * time.Millisecond)
mu.Lock()
finalCount := expiredCount
mu.Unlock()
if finalCount != 0 {
t.Errorf("Expected 0 timers to expire after CancelAll, got %d", finalCount)
}
}
func TestCancelAll_EmptyManager(t *testing.T) {
tm := NewTTLManager()
tm.CancelAll()
tm.mu.RLock()
if len(tm.timers) != 0 {
t.Errorf("Expected empty timers map, got %d entries", len(tm.timers))
}
tm.mu.RUnlock()
}
func TestConcurrentAccess(t *testing.T) {
tm := NewTTLManager()
var wg sync.WaitGroup
numGoroutines := 10
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
vmID := fmt.Sprintf("concurrent-vm-%d", id)
cancelFunc := tm.SetTTL(vmID, 100*time.Millisecond, func() {})
// Sometimes cancel immediately
if id%2 == 0 {
cancelFunc()
}
}(i)
}
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
vmID := fmt.Sprintf("concurrent-vm-%d", id)
time.Sleep(10 * time.Millisecond)
tm.CancelTTL(vmID)
}(i)
}
wg.Wait()
tm.CancelAll()
// This test primarily checks that no race conditions occur
// The actual state at the end is unpredictable due to timing
}
func TestSetTTL_ZeroDuration(t *testing.T) {
tm := NewTTLManager()
mu := sync.Mutex{}
expired := false
vmID := "zero-duration-vm"
tm.SetTTL(vmID, 0, func() {
mu.Lock()
defer mu.Unlock()
expired = true
})
time.Sleep(10 * time.Millisecond)
mu.Lock()
if !expired {
t.Error("TTL with zero duration should expire immediately")
}
mu.Unlock()
}
func TestSetTTL_NegativeDuration(t *testing.T) {
tm := NewTTLManager()
mu := sync.Mutex{}
expired := false
vmID := "negative-duration-vm"
tm.SetTTL(vmID, -100*time.Millisecond, func() {
mu.Lock()
defer mu.Unlock()
expired = true
})
time.Sleep(10 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if !expired {
t.Error("TTL with negative duration should expire immediately")
}
}
+1 -49
View File
@@ -6,10 +6,8 @@
package mocks
import (
agent "github.com/ultravioletrs/cocos/agent"
manager "github.com/ultravioletrs/cocos/pkg/manager"
mock "github.com/stretchr/testify/mock"
manager "github.com/ultravioletrs/cocos/pkg/manager"
)
// VM is an autogenerated mock type for the VM type
@@ -162,52 +160,6 @@ func (_c *VM_GetProcess_Call) RunAndReturn(run func() int) *VM_GetProcess_Call {
return _c
}
// SendAgentConfig provides a mock function with given fields: ac
func (_m *VM) SendAgentConfig(ac agent.Computation) error {
ret := _m.Called(ac)
if len(ret) == 0 {
panic("no return value specified for SendAgentConfig")
}
var r0 error
if rf, ok := ret.Get(0).(func(agent.Computation) error); ok {
r0 = rf(ac)
} else {
r0 = ret.Error(0)
}
return r0
}
// VM_SendAgentConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendAgentConfig'
type VM_SendAgentConfig_Call struct {
*mock.Call
}
// SendAgentConfig is a helper method to define mock.On call
// - ac agent.Computation
func (_e *VM_Expecter) SendAgentConfig(ac interface{}) *VM_SendAgentConfig_Call {
return &VM_SendAgentConfig_Call{Call: _e.mock.On("SendAgentConfig", ac)}
}
func (_c *VM_SendAgentConfig_Call) Run(run func(ac agent.Computation)) *VM_SendAgentConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(agent.Computation))
})
return _c
}
func (_c *VM_SendAgentConfig_Call) Return(_a0 error) *VM_SendAgentConfig_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *VM_SendAgentConfig_Call) RunAndReturn(run func(agent.Computation) error) *VM_SendAgentConfig_Call {
_c.Call.Return(run)
return _c
}
// SetProcess provides a mock function with given fields: pid
func (_m *VM) SetProcess(pid int) error {
ret := _m.Called(pid)
-2
View File
@@ -5,7 +5,6 @@ package vm
import (
"log/slog"
"github.com/ultravioletrs/cocos/agent"
pkgmanager "github.com/ultravioletrs/cocos/pkg/manager"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -14,7 +13,6 @@ import (
type VM interface {
Start() error
Stop() error
SendAgentConfig(ac agent.Computation) error
SetProcess(pid int) error
GetProcess() int
GetCID() int
+24
View File
@@ -20,6 +20,11 @@ packages:
dir: "{{.InterfaceDir}}/mocks"
filename: "agent_grpc_algo.go"
mockname: "{{.InterfaceName}}"
AgentService_IMAMeasurementsClient:
config:
dir: "{{.InterfaceDir}}/mocks"
filename: "agent_grpc_ima.go"
mockname: "{{.InterfaceName}}"
github.com/ultravioletrs/cocos/agent/auth:
interfaces:
Authenticator:
@@ -119,3 +124,22 @@ packages:
dir: "{{.InterfaceDir}}/mocks"
filename: "attestation.go"
mockname: "{{.InterfaceName}}"
Verifier:
config:
dir: "{{.InterfaceDir}}/mocks"
filename: "verifier.go"
mockname: "{{.InterfaceName}}"
github.com/ultravioletrs/cocos/agent/algorithm:
interfaces:
Algorithm:
config:
dir: "{{.InterfaceDir}}/mocks"
filename: "algorithm.go"
mockname: "{{.InterfaceName}}"
github.com/ultravioletrs/cocos/pkg/attestation/cmdconfig:
interfaces:
MeasurementProvider:
config:
dir: "{{.InterfaceDir}}/mocks"
filename: "measurement_provider.go"
mockname: "{{.InterfaceName}}"
+333
View File
@@ -0,0 +1,333 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"strings"
"time"
"github.com/absmach/certs"
certscli "github.com/absmach/certs/cli"
"github.com/absmach/certs/errors"
certssdk "github.com/absmach/certs/sdk"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"golang.org/x/crypto/sha3"
)
const (
vmpl2 = 2
organization = "Ultraviolet"
country = "Serbia"
province = ""
locality = "Belgrade"
streetAddress = "Bulevar Arsenija Carnojevica 103"
postalCode = "11000"
notAfterYear = 1
notAfterMonth = 0
notAfterDay = 0
)
var (
SNPvTPMOID = asn1.ObjectIdentifier{2, 99999, 1, 0}
AzureOID = asn1.ObjectIdentifier{2, 99999, 1, 1}
TDXOID = asn1.ObjectIdentifier{2, 99999, 1, 2}
)
type csrReq struct {
CSR string `json:"csr,omitempty"`
}
func getPlatformProvider(platformType attestation.PlatformType) (attestation.Provider, error) {
switch platformType {
case attestation.SNPvTPM:
return vtpm.NewProvider(true, vmpl2), nil
case attestation.Azure:
return azure.NewProvider(), nil
case attestation.TDX:
return tdx.NewProvider(), nil
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
}
func getPlatformVerifier(platformType attestation.PlatformType) (attestation.Verifier, error) {
var verifier attestation.Verifier
switch platformType {
case attestation.SNPvTPM:
verifier = vtpm.NewVerifier(nil)
case attestation.Azure:
verifier = azure.NewVerifier(nil)
case attestation.TDX:
verifier = tdx.NewVerifier()
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
err := verifier.JSONToPolicy(attestation.AttestationPolicyPath)
if err != nil {
return nil, err
}
return verifier, nil
}
func getOID(platformType attestation.PlatformType) (asn1.ObjectIdentifier, error) {
switch platformType {
case attestation.SNPvTPM:
return SNPvTPMOID, nil
case attestation.Azure:
return AzureOID, nil
case attestation.TDX:
return TDXOID, nil
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
}
func GetPlatformTypeFromOID(oid asn1.ObjectIdentifier) (attestation.PlatformType, error) {
switch {
case oid.Equal(SNPvTPMOID):
return attestation.SNPvTPM, nil
case oid.Equal(AzureOID):
return attestation.Azure, nil
case oid.Equal(TDXOID):
return attestation.TDX, nil
default:
return 0, fmt.Errorf("unsupported OID: %v", oid)
}
}
func VerifyCertificateExtension(extension []byte, pubKey []byte, nonce []byte, pType attestation.PlatformType) error {
teeNonce := append(pubKey, nonce...)
hashNonce := sha3.Sum512(teeNonce)
verifier, err := getPlatformVerifier(pType)
if err != nil {
return fmt.Errorf("failed to get platform verifier: %w", err)
}
if err = verifier.VerifyAttestation(extension, hashNonce[:], hashNonce[:vtpm.Nonce]); err != nil {
fmt.Printf("failed to verify attestation: %v\n", err)
return err
}
return nil
}
func GetCertificate(caUrl string, cvmId string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
pType := attestation.CCPlatform()
provider, err := getPlatformProvider(pType)
if err != nil {
return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, fmt.Errorf("failed to get platform provider: %w", err)
}
}
teeOid, err := getOID(pType)
if err != nil {
return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return nil, fmt.Errorf("failed to get OID for platform type: %w", err)
}
}
return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
curve := elliptic.P256()
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate private/public key: %w", err)
}
pubKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal public key to DER format: %w", err)
}
sniLength := len(clientHello.ServerName)
if sniLength < 7 || clientHello.ServerName[sniLength-6:] != ".nonce" {
return nil, fmt.Errorf("invalid server name: %s", clientHello.ServerName)
}
nonceStr := clientHello.ServerName[:sniLength-6]
nonce, err := hex.DecodeString(nonceStr)
if err != nil {
return nil, fmt.Errorf("failed to decode nonce from server name: %w", err)
}
if len(nonce) != 64 {
return nil, fmt.Errorf("invalid nonce length: expected 64 bytes, got %d bytes", len(nonce))
}
attestExtension, err := getCertificateExtension(provider, pubKeyDER, nonce, teeOid)
if err != nil {
return nil, fmt.Errorf("failed to get certificate extension: %w", err)
}
var certDERBytes []byte
if caUrl == "" && cvmId == "" {
certTemplate := &x509.Certificate{
SerialNumber: big.NewInt(202403311),
Subject: pkix.Name{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
ExtraExtensions: []pkix.Extension{attestExtension},
}
DERBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
certDERBytes = DERBytes
} else {
csrmd := certs.CSRMetadata{
Organization: []string{organization},
Country: []string{country},
Province: []string{province},
Locality: []string{locality},
StreetAddress: []string{streetAddress},
PostalCode: []string{postalCode},
ExtraExtensions: []pkix.Extension{attestExtension},
}
csr, err := certscli.CreateCSR(csrmd, privateKey)
if err != nil {
return nil, fmt.Errorf("failed to create CSR: %w", err)
}
csrData := string(csr.CSR)
r := csrReq{
CSR: csrData,
}
data, sdkErr := json.Marshal(r)
if sdkErr != nil {
return nil, fmt.Errorf("failed to marshal CSR request: %w", sdkErr)
}
notBefore := time.Now()
notAfter := time.Now().AddDate(notAfterYear, notAfterMonth, notAfterDay)
ttlString := notAfter.Sub(notBefore).String()
query := url.Values{}
query.Add("ttl", ttlString)
query_string := query.Encode()
certsEndpoint := "certs"
csrEndpoint := "csrs"
endpoint := fmt.Sprintf("%s/%s/%s", certsEndpoint, csrEndpoint, cvmId)
url := fmt.Sprintf("%s/%s?%s", caUrl, endpoint, query_string)
_, body, err := processRequest(http.MethodPost, url, data, nil, http.StatusOK)
if err != nil {
return nil, fmt.Errorf("failed to process request: %w", err)
}
var cert certssdk.Certificate
if err := json.Unmarshal(body, &cert); err != nil {
return nil, fmt.Errorf("failed to unmarshal certificate response: %w", err)
}
cleanCertificateString := strings.ReplaceAll(cert.Certificate, "\\n", "\n")
block, rest := pem.Decode([]byte(cleanCertificateString))
if len(rest) != 0 {
return nil, fmt.Errorf("failed to convert generated certificate to DER format: %s", cleanCertificateString)
}
certDERBytes = block.Bytes
}
return &tls.Certificate{
Certificate: [][]byte{certDERBytes},
PrivateKey: privateKey,
}, nil
}
}
func getCertificateExtension(provider attestation.Provider, pubKey []byte, nonce []byte, teeOid asn1.ObjectIdentifier) (pkix.Extension, error) {
teeNonce := append(pubKey, nonce...)
hashNonce := sha3.Sum512(teeNonce)
rawAttestation, err := provider.Attestation(hashNonce[:], hashNonce[:vtpm.Nonce])
if err != nil {
return pkix.Extension{}, fmt.Errorf("failed to get attestation: %w", err)
}
return pkix.Extension{
Id: teeOid,
Value: rawAttestation,
}, nil
}
func processRequest(method, reqUrl string, data []byte, headers map[string]string, expectedRespCodes ...int) (http.Header, []byte, errors.SDKError) {
req, err := http.NewRequest(method, reqUrl, bytes.NewReader(data))
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
// Sets a default value for the Content-Type.
// Overridden if Content-Type is passed in the headers arguments.
req.Header.Add("Content-Type", "application/json")
for key, value := range headers {
req.Header.Add(key, value)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
resp, err := client.Do(req)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
defer resp.Body.Close()
sdkerr := errors.CheckError(resp, expectedRespCodes...)
if sdkerr != nil {
return make(http.Header), []byte{}, sdkerr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
return resp.Header, body, nil
}
-429
View File
@@ -1,429 +0,0 @@
// Code generated by cmd/cgo; DO NOT EDIT.
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
// #cgo LDFLAGS: -lssl -lcrypto
// #include "extensions.h"
// #include <string.h>
import "C"
import (
"crypto/sha3"
"fmt"
"io"
"net"
"os"
"strconv"
"sync"
"syscall"
"time"
"unsafe"
"github.com/absmach/magistrala/pkg/errors"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
const (
noError = 0
errorZeroReturn = 6
errorWantRead = 2
errorWantWrite = 3
errorSyscall = 5
errorSsl = 1
waitTime = 2
vmpl2 = 2
)
var (
errListener = errors.New("listener could not be created")
errBadIPFormat = errors.New("bad format of IP address")
errCloseTLS = errors.New("could not close TLS connection")
errConnFailed = errors.New("tls connection is nil")
errWrite = errors.New("could not write to TLS")
errTLSConn = errors.New("connection did not close correctly")
errReadDeadline = errors.New("could not set read deadline, socket timeout failed")
errWriteDeadline = errors.New("could not set write deadline, socket timeout failed")
errConnCreate = errors.New("could not create connection")
)
func formTeeData(pubKey []byte, teeNonce []byte) []byte {
combined := append(pubKey, teeNonce...)
sum := sha3.Sum512(combined)
return sum[:]
}
func getPlatformProvider(platformType attestation.PlatformType, pubKey []byte) (attestation.Provider, error) {
switch platformType {
case attestation.SNPvTPM:
return vtpm.New(pubKey, true, vmpl2, nil), nil
case attestation.Azure:
return azure.New(nil), nil
default:
return nil, fmt.Errorf("unsupported platform type: %d", platformType)
}
}
//export callVerificationValidationCallback
func callVerificationValidationCallback(platformType C.int, pubKey *C.uchar, pubKeyLen C.int, attestReport *C.uchar, attestReportSize C.int, teeNonceByte *C.uchar, vTPMNonceByte *C.uchar) C.int {
pubKeyCert := C.GoBytes(unsafe.Pointer(pubKey), pubKeyLen)
teeNonceData := C.GoBytes(unsafe.Pointer(teeNonceByte), quoteprovider.Nonce)
vTPMNonce := C.GoBytes(unsafe.Pointer(vTPMNonceByte), vtpm.Nonce)
pType := attestation.PlatformType(int(platformType))
attestationReport := C.GoBytes(unsafe.Pointer(attestReport), attestReportSize)
teeData := formTeeData(pubKeyCert, teeNonceData)
provider, err := getPlatformProvider(pType, pubKeyCert)
if err != nil {
fmt.Fprintf(os.Stderr, "no attestation provider found for platform type %s", err.Error())
return C.int(-1)
}
err = provider.VerifyAttestation(attestationReport, teeData, vTPMNonce)
if err != nil {
fmt.Fprintf(os.Stderr, "verification callback failed %s", err.Error())
return C.int(-1)
}
return C.int(0)
}
//export callFetchAttestationCallback
func callFetchAttestationCallback(platformType C.int, pubKey *C.uchar, pubKeyLen C.int, teeNonceByte *C.uchar, vTPMNonceByte *C.uchar, outlen *C.ulong) *C.uchar {
pubKeyCert := C.GoBytes(unsafe.Pointer(pubKey), pubKeyLen)
teeNonceData := C.GoBytes(unsafe.Pointer(teeNonceByte), quoteprovider.Nonce)
vTPMNonce := C.GoBytes(unsafe.Pointer(vTPMNonceByte), vtpm.Nonce)
pType := attestation.PlatformType(int(platformType))
teeData := formTeeData(pubKeyCert, teeNonceData)
provider, err := getPlatformProvider(pType, pubKeyCert)
if err != nil {
fmt.Fprintf(os.Stderr, "no attestation provider found for platform type %s", err.Error())
return nil
}
quote, err := provider.Attestation(teeData, vTPMNonce)
if err != nil {
fmt.Fprintf(os.Stderr, "attestation callback returned nil: %s", err.Error())
return nil
}
*outlen = C.ulong(len(quote))
resultC := C.malloc(C.size_t(len(quote)))
if resultC == nil {
fmt.Fprintf(os.Stderr, "could not allocate memory for fetch attestation callback")
return nil
}
C.memcpy(resultC, unsafe.Pointer(&quote[0]), C.size_t(len(quote)))
return (*C.uchar)(resultC)
}
//export returnCCPlatformType
func returnCCPlatformType() int32 {
return int32(attestation.CCPlatform())
}
type ATLSServerListener struct {
tlsListener *C.tls_server_connection
}
func Listen(addr string, cert []byte, key []byte) (net.Listener, error) {
ip, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, errors.Wrap(errListener, err)
}
p, err := strconv.Atoi(port)
if err != nil {
return nil, errors.Wrap(errBadIPFormat, err)
}
cCertPEM := (*C.char)(unsafe.Pointer(&cert[0]))
cKeyPEM := (*C.char)(unsafe.Pointer(&key[0]))
cIP := C.CString(ip)
defer C.free(unsafe.Pointer(cIP))
atlsListener := C.start_tls_server(
cCertPEM, C.int(len(cert)),
cKeyPEM, C.int(len(key)),
cIP, C.int(p))
if atlsListener == nil {
return nil, errors.Wrap(errListener, err)
}
return &ATLSServerListener{tlsListener: atlsListener}, nil
}
// accept implements the Accept method in the [Listener] interface; it
// waits for the next call and returns a generic [Conn].
func (l *ATLSServerListener) Accept() (net.Conn, error) {
conn := C.tls_server_accept(l.tlsListener)
if conn == nil {
return &ATLSConn{tlsConn: nil}, nil
}
return &ATLSConn{tlsConn: conn}, nil
}
// close stops listening on the TCP address.
// already Accepted connections are not closed.
func (l *ATLSServerListener) Close() error {
ret := C.tls_server_close(l.tlsListener)
if ret != 0 {
return errCloseTLS
}
return nil
}
// addr returns the listener's network address, a [*TCPAddr].
// the Addr returned is shared by all invocations of Addr, so
// do not modify it.
func (l *ATLSServerListener) Addr() net.Addr {
cIP := C.tls_return_addr(&l.tlsListener.addr)
defer C.free(unsafe.Pointer(cIP))
ip := C.GoString(cIP)
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return nil
}
port := C.tls_return_port(&l.tlsListener.addr)
return &net.TCPAddr{IP: parsedIP, Port: int(port)}
}
type ATLSConn struct {
tlsConn *C.tls_connection
fdReadMutex sync.Mutex
fdWriteMutex sync.Mutex
fdDelayMutex sync.Mutex
}
func (c *ATLSConn) Read(b []byte) (int, error) {
c.fdReadMutex.Lock()
defer c.fdReadMutex.Unlock()
if c.tlsConn == nil {
return 0, errConnFailed
}
n := int(C.tls_read(c.tlsConn, unsafe.Pointer(&b[0]), C.int(len(b))))
if n > 0 {
return n, nil
}
// call the C function SSL_get_error to interpret the error.
errCode := int(C.SSL_get_error(c.tlsConn.ssl, C.int(n)))
// handle specific error codes returned by SSL_get_error.
switch errCode {
case noError:
return n, nil // no error.
case errorZeroReturn:
fmt.Fprintf(os.Stdout, "Connection closed by peer\n")
return 0, io.EOF // connection closed.
case errorWantRead:
fmt.Fprintf(os.Stderr, "Operation read incomplete, retry later\n")
return 0, nil // non-fatal, just retry later.
case errorWantWrite:
fmt.Fprintf(os.Stderr, "Operation write incomplete, retry later\n")
return 0, nil // non-fatal, just retry later.
case errorSyscall:
fmt.Fprintf(os.Stderr, "I/O error\n")
return 0, syscall.ECONNRESET // return connection reset error.
case errorSsl:
fmt.Fprintf(os.Stderr, "I/O error\n")
return 0, syscall.ECONNRESET // return connection reset error.
default:
fmt.Fprintf(os.Stderr, "SSL error occurred: %d\n", errCode)
return 0, fmt.Errorf("SSL error")
}
}
func (c *ATLSConn) Write(b []byte) (int, error) {
c.fdWriteMutex.Lock()
defer c.fdWriteMutex.Unlock()
if c.tlsConn == nil {
return 0, errConnFailed
}
n := int(C.tls_write(c.tlsConn, unsafe.Pointer(&b[0]), C.int(len(b))))
if n < 0 {
return 0, errWrite
}
return n, nil
}
func (c *ATLSConn) Close() error {
c.fdReadMutex.Lock()
defer c.fdReadMutex.Unlock()
c.fdWriteMutex.Lock()
defer c.fdWriteMutex.Unlock()
c.fdDelayMutex.Lock()
defer c.fdDelayMutex.Unlock()
if c.tlsConn == nil {
return nil
}
for {
ret := C.tls_close(c.tlsConn)
if int(ret) == 0 {
c.fdDelayMutex.Unlock()
c.fdWriteMutex.Unlock()
c.fdReadMutex.Unlock()
time.Sleep(waitTime * time.Millisecond)
c.fdDelayMutex.Lock()
c.fdWriteMutex.Lock()
c.fdReadMutex.Lock()
} else if int(ret) < 0 {
c.tlsConn = nil
return errTLSConn
} else if int(ret) == 1 {
c.tlsConn = nil
break
}
}
return nil
}
func (c *ATLSConn) LocalAddr() net.Addr {
if c.tlsConn == nil {
return nil
}
cIP := C.tls_return_addr(&c.tlsConn.local_addr)
ipLength := C.strlen(cIP)
defer C.free(unsafe.Pointer(cIP))
ip := C.GoStringN(cIP, C.int(ipLength))
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
fmt.Println("Invalid IP address")
return nil
}
port := C.tls_return_port(&c.tlsConn.local_addr)
return &net.TCPAddr{IP: parsedIP, Port: int(port)}
}
func (c *ATLSConn) RemoteAddr() net.Addr {
if c.tlsConn == nil {
return nil
}
cIP := C.tls_return_addr(&c.tlsConn.remote_addr)
if cIP == nil {
fmt.Println("RemoteAddr error while fetching ip")
return nil
}
ipLength := C.strlen(cIP)
defer C.free(unsafe.Pointer(cIP))
ip := C.GoStringN(cIP, C.int(ipLength))
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
fmt.Println("Invalid IP address")
return nil
}
port := C.tls_return_port(&c.tlsConn.remote_addr)
return &net.TCPAddr{IP: parsedIP, Port: int(port)}
}
func (c *ATLSConn) SetDeadline(t time.Time) error {
c.fdDelayMutex.Lock()
defer c.fdDelayMutex.Unlock()
if c.tlsConn == nil {
return nil
}
sec, usec := timeToTimeout(t)
if C.set_socket_read_timeout(c.tlsConn, C.int(sec), C.int(usec)) < 0 {
return errReadDeadline
}
if C.set_socket_write_timeout(c.tlsConn, C.int(sec), C.int(usec)) < 0 {
return errWriteDeadline
}
return nil
}
func (c *ATLSConn) SetReadDeadline(t time.Time) error {
c.fdDelayMutex.Lock()
defer c.fdDelayMutex.Unlock()
if c.tlsConn == nil {
return nil
}
sec, usec := timeToTimeout(t)
if C.set_socket_read_timeout(c.tlsConn, C.int(sec), C.int(usec)) < 0 {
return errReadDeadline
}
return nil
}
func (c *ATLSConn) SetWriteDeadline(t time.Time) error {
c.fdDelayMutex.Lock()
defer c.fdDelayMutex.Unlock()
if c.tlsConn == nil {
return nil
}
sec, usec := timeToTimeout(t)
if C.set_socket_write_timeout(c.tlsConn, C.int(sec), C.int(usec)) < 0 {
return errWriteDeadline
}
return nil
}
func DialTLSClient(hostname string, port int) (net.Conn, error) {
cHostName := C.CString(hostname)
defer C.free(unsafe.Pointer(cHostName))
conn := C.new_tls_connection(cHostName, C.int(port))
if conn == nil {
return nil, errConnCreate
}
return &ATLSConn{tlsConn: conn}, nil
}
func timeToTimeout(t time.Time) (int, int) {
if t.IsZero() {
return 0, 0
}
d := time.Until(t)
if d <= 0 {
return 0, 0
}
seconds := int(d.Seconds())
microseconds := int(d.Nanoseconds()/1000) % 1_000_000
return seconds, microseconds
}
-101
View File
@@ -1,101 +0,0 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"testing"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestListen(t *testing.T) {
cert := []byte("dummy_cert")
key := []byte("dummy_key")
cases := []struct {
name string
address string
err error
}{
{
name: "Valid address",
address: "127.0.0.1:8889",
err: nil,
},
{
name: "Invalid address format",
address: "127.0.0.1",
err: errListener,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
l, err := Listen(c.address, cert, key)
assert.True(t, errors.Contains(err, c.err))
if l != nil {
t.Cleanup(func() {
err := l.Close()
assert.NoError(t, err)
})
}
})
}
}
func TestATLSServerListener_Accept(t *testing.T) {
t.Run("Accepts connection", func(t *testing.T) {
listener, err := Listen("127.0.0.1:8887", []byte("dummy_cert"), []byte("dummy_key"))
assert.NoError(t, err)
t.Cleanup(func() {
err := listener.Close()
assert.NoError(t, err)
})
conn, err := listener.Accept()
assert.NoError(t, err)
assert.NotNil(t, conn)
})
}
func TestATLSConn_Read(t *testing.T) {
buffer := make([]byte, 1024)
t.Run("Read with nil connection", func(t *testing.T) {
conn := &ATLSConn{tlsConn: nil}
_, err := conn.Read(buffer)
assert.Error(t, err)
assert.Equal(t, err, errConnFailed)
})
}
func TestATLSConn_Write(t *testing.T) {
data := []byte("test data")
t.Run("Write with nil connection", func(t *testing.T) {
conn := &ATLSConn{tlsConn: nil}
_, err := conn.Write(data)
assert.Error(t, err)
assert.Equal(t, err, errConnFailed)
})
}
func TestATLSConn_DeadlineFunctions(t *testing.T) {
conn := &ATLSConn{}
t.Run("SetDeadline - valid time", func(t *testing.T) {
err := conn.SetDeadline(time.Now().Add(1 * time.Minute))
assert.NoError(t, err)
})
t.Run("SetReadDeadline - past time", func(t *testing.T) {
err := conn.SetReadDeadline(time.Now().Add(-1 * time.Minute))
assert.NoError(t, err)
})
t.Run("SetWriteDeadline - zero time", func(t *testing.T) {
err := conn.SetWriteDeadline(time.Time{})
assert.NoError(t, err)
})
}
+624
View File
@@ -0,0 +1,624 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
certssdk "github.com/absmach/certs/sdk"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/mocks"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"golang.org/x/crypto/sha3"
"google.golang.org/protobuf/encoding/protojson"
)
const sevProductNameMilan = "Milan"
var policy = attestation.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &attestation.PcrConfig{}}
func TestGetPlatformProvider(t *testing.T) {
cases := []struct {
name string
platformType attestation.PlatformType
expectedError error
}{
{
name: "Valid platform type SNPvTPM",
platformType: attestation.SNPvTPM,
expectedError: nil,
},
{
name: "Valid platform type Azure",
platformType: attestation.Azure,
expectedError: nil,
},
{
name: "Valid platform type TDX",
platformType: attestation.TDX,
expectedError: nil,
},
{
name: "Invalid platform type",
platformType: 999,
expectedError: errors.New("unsupported platform type: 999"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
provider, err := getPlatformProvider(c.platformType)
if c.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, c.expectedError.Error(), err.Error())
assert.Nil(t, provider)
} else {
assert.NoError(t, err)
assert.NotNil(t, provider)
}
})
}
}
func TestGetPlatformVerifier(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
cases := []struct {
name string
platformType attestation.PlatformType
expectedError error
}{
{
name: "Valid platform type SNPvTPM",
platformType: attestation.SNPvTPM,
expectedError: nil,
},
{
name: "Valid platform type Azure",
platformType: attestation.Azure,
expectedError: nil,
},
{
name: "Valid platform type TDX",
platformType: attestation.TDX,
expectedError: errors.New("unknown field \"pcr_values\""),
},
{
name: "Invalid platform type",
platformType: 999,
expectedError: errors.New("unsupported platform type: 999"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
verifier, err := getPlatformVerifier(c.platformType)
if c.expectedError != nil {
assert.Error(t, err)
assert.Contains(t, err.Error(), c.expectedError.Error())
assert.Nil(t, verifier)
} else {
assert.NoError(t, err)
assert.NotNil(t, verifier)
}
})
}
}
func TestGetOID(t *testing.T) {
cases := []struct {
name string
platformType attestation.PlatformType
expectedOID asn1.ObjectIdentifier
expectedError error
}{
{
name: "Valid platform type SNPvTPM",
platformType: attestation.SNPvTPM,
expectedOID: SNPvTPMOID,
expectedError: nil,
},
{
name: "Valid platform type Azure",
platformType: attestation.Azure,
expectedOID: AzureOID,
expectedError: nil,
},
{
name: "Valid platform type TDX",
platformType: attestation.TDX,
expectedOID: TDXOID,
expectedError: nil,
},
{
name: "Invalid platform type",
platformType: 999,
expectedOID: nil,
expectedError: errors.New("unsupported platform type: 999"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
oid, err := getOID(c.platformType)
if c.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, c.expectedError.Error(), err.Error())
assert.Nil(t, oid)
} else {
assert.NoError(t, err)
assert.Equal(t, c.expectedOID, oid)
}
})
}
}
func TestGetPlatformTypeFromOID(t *testing.T) {
cases := []struct {
name string
oid asn1.ObjectIdentifier
expectedType attestation.PlatformType
expectedError error
}{
{
name: "Valid OID for SNPvTPM",
oid: SNPvTPMOID,
expectedType: attestation.SNPvTPM,
expectedError: nil,
},
{
name: "Valid OID for Azure",
oid: AzureOID,
expectedType: attestation.Azure,
expectedError: nil,
},
{
name: "Valid OID for TDX",
oid: TDXOID,
expectedType: attestation.TDX,
expectedError: nil,
},
{
name: "Invalid OID",
oid: asn1.ObjectIdentifier{1, 2, 3},
expectedType: 0,
expectedError: errors.New("unsupported OID: 1.2.3"),
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
pType, err := GetPlatformTypeFromOID(c.oid)
if c.expectedError != nil {
assert.Error(t, err)
assert.Equal(t, c.expectedError.Error(), err.Error())
assert.Equal(t, attestation.PlatformType(0), pType)
} else {
assert.NoError(t, err)
assert.Equal(t, c.expectedType, pType)
}
})
}
}
func TestVerifyCertificateExtension(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
pubKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
require.NoError(t, err)
nonce := make([]byte, 64)
_, err = rand.Read(nonce)
require.NoError(t, err)
teeNonce := append(pubKeyDER, nonce...)
hashNonce := sha3.Sum512(teeNonce)
cases := []struct {
name string
extension []byte
pubKey []byte
nonce []byte
platformType attestation.PlatformType
expectError bool
}{
{
name: "Valid extension with SNPvTPM",
extension: hashNonce[:],
pubKey: pubKeyDER,
nonce: nonce,
platformType: attestation.SNPvTPM,
expectError: true,
},
{
name: "Invalid platform type",
extension: hashNonce[:],
pubKey: pubKeyDER,
nonce: nonce,
platformType: 999,
expectError: true,
},
{
name: "Empty extension",
extension: []byte{},
pubKey: pubKeyDER,
nonce: nonce,
platformType: attestation.SNPvTPM,
expectError: true,
},
{
name: "Empty public key",
extension: hashNonce[:],
pubKey: []byte{},
nonce: nonce,
platformType: attestation.SNPvTPM,
expectError: true,
},
{
name: "Empty nonce",
extension: hashNonce[:],
pubKey: pubKeyDER,
nonce: []byte{},
platformType: attestation.SNPvTPM,
expectError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := VerifyCertificateExtension(c.extension, c.pubKey, c.nonce, c.platformType)
if c.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestGetCertificateExtension(t *testing.T) {
mockProvider := new(mocks.Provider)
mockProvider.On("Attestation", mock.Anything, mock.Anything).Return([]byte("mock-attestation-data"), nil)
pubKey := []byte("test-public-key")
nonce := make([]byte, 32)
_, err := rand.Read(nonce)
require.NoError(t, err)
testOID := asn1.ObjectIdentifier{1, 2, 3, 4}
extension, err := getCertificateExtension(mockProvider, pubKey, nonce, testOID)
assert.NoError(t, err)
assert.Equal(t, testOID, extension.Id)
assert.Equal(t, []byte("mock-attestation-data"), extension.Value)
}
func TestGetCertificateWithSelfSigned(t *testing.T) {
getCertFunc := GetCertificate("", "")
nonce := make([]byte, 64)
_, err := rand.Read(nonce)
require.NoError(t, err)
serverName := hex.EncodeToString(nonce) + ".nonce"
clientHello := &tls.ClientHelloInfo{
ServerName: serverName,
}
cert, err := getCertFunc(clientHello)
if err != nil {
t.Logf("Expected error due to missing attestation setup: %v", err)
assert.Error(t, err)
} else {
assert.NotNil(t, cert)
assert.NotEmpty(t, cert.Certificate)
assert.NotNil(t, cert.PrivateKey)
}
}
func TestGetCertificateWithCA(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
mockCert := certssdk.Certificate{
Certificate: "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1UZXN0IENBIFJvb3QwHhcNMjMwMzMxMDAwMDAwWhcNMjQwMzMxMDAwMDAwWjAYMRYwFAYDVQQDDA1UZXN0IENlcnRpZmljYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtest-key-data-here\n-----END CERTIFICATE-----",
}
response, _ := json.Marshal(mockCert)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(response); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
}))
defer mockServer.Close()
getCertFunc := GetCertificate(mockServer.URL, "test-cvm-id")
nonce := make([]byte, 64)
_, err := rand.Read(nonce)
require.NoError(t, err)
serverName := hex.EncodeToString(nonce) + ".nonce"
clientHello := &tls.ClientHelloInfo{
ServerName: serverName,
}
_, err = getCertFunc(clientHello)
if err != nil {
t.Logf("Expected error due to missing attestation setup: %v", err)
assert.Error(t, err)
}
}
func TestGetCertificateInvalidServerName(t *testing.T) {
getCertFunc := GetCertificate("", "")
cases := []struct {
name string
serverName string
expectErr string
}{
{
name: "Missing .nonce suffix",
serverName: "invalidname",
expectErr: "failed to get platform provider",
},
{
name: "Too short server name",
serverName: "short",
expectErr: "failed to get platform provider",
},
{
name: "Invalid nonce encoding",
serverName: "invalidhex.nonce",
expectErr: "failed to get platform provider",
},
{
name: "Wrong nonce length",
serverName: "deadbeef.nonce",
expectErr: "failed to get platform provider",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
clientHello := &tls.ClientHelloInfo{
ServerName: c.serverName,
}
cert, err := getCertFunc(clientHello)
assert.Error(t, err)
assert.Contains(t, err.Error(), c.expectErr)
assert.Nil(t, cert)
})
}
}
func TestProcessRequest(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/success":
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"message": "success"}`)); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
case "/notfound":
w.WriteHeader(http.StatusNotFound)
if _, err := w.Write([]byte(`{"error": "not found"}`)); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
case "/headers":
if r.Header.Get("X-Custom-Header") == "test-value" {
w.Header().Set("X-Response-Header", "received")
}
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte(`{"headers": "ok"}`)); err != nil {
http.Error(w, "Failed to write response", http.StatusInternalServerError)
return
}
default:
w.WriteHeader(http.StatusInternalServerError)
}
}))
defer testServer.Close()
cases := []struct {
name string
method string
url string
data []byte
headers map[string]string
expectedRespCodes []int
expectError bool
}{
{
name: "Successful GET request",
method: http.MethodGet,
url: testServer.URL + "/success",
data: nil,
headers: nil,
expectedRespCodes: []int{http.StatusOK},
expectError: false,
},
{
name: "Successful POST request with data",
method: http.MethodPost,
url: testServer.URL + "/success",
data: []byte(`{"test": "data"}`),
headers: nil,
expectedRespCodes: []int{http.StatusOK},
expectError: false,
},
{
name: "Request with custom headers",
method: http.MethodGet,
url: testServer.URL + "/headers",
data: nil,
headers: map[string]string{"X-Custom-Header": "test-value"},
expectedRespCodes: []int{http.StatusOK},
expectError: false,
},
{
name: "Request with unexpected status code",
method: http.MethodGet,
url: testServer.URL + "/notfound",
data: nil,
headers: nil,
expectedRespCodes: []int{http.StatusOK},
expectError: true,
},
{
name: "Request with multiple expected status codes",
method: http.MethodGet,
url: testServer.URL + "/notfound",
data: nil,
headers: nil,
expectedRespCodes: []int{http.StatusOK, http.StatusNotFound},
expectError: false,
},
{
name: "Request to invalid URL",
method: http.MethodGet,
url: "invalid-url",
data: nil,
headers: nil,
expectedRespCodes: []int{http.StatusOK},
expectError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
headers, body, err := processRequest(c.method, c.url, c.data, c.headers, c.expectedRespCodes...)
if c.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.NotNil(t, headers)
assert.NotNil(t, body)
if c.name == "Request with custom headers" {
assert.Equal(t, "received", headers.Get("X-Response-Header"))
}
}
})
}
}
func TestGetCertificateExtensionError(t *testing.T) {
mockProvider := new(mocks.Provider)
mockProvider.On("Attestation", mock.Anything, mock.Anything).Return(nil, errors.New("failed to get attestation"))
pubKey := []byte("test-public-key")
nonce := make([]byte, 32)
testOID := asn1.ObjectIdentifier{1, 2, 3, 4}
extension, err := getCertificateExtension(mockProvider, pubKey, nonce, testOID)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to get attestation")
assert.Equal(t, pkix.Extension{}, extension)
}
func prepVerifyAttReport(t *testing.T) *sevsnp.Attestation {
file, err := os.ReadFile("../../attestation.bin")
require.NoError(t, err)
if len(file) < abi.ReportSize {
file = append(file, make([]byte, abi.ReportSize-len(file))...)
}
rr, err := abi.ReportCertsToProto(file)
require.NoError(t, err)
return rr
}
func setAttestationPolicy(rr *sevsnp.Attestation, policyDirectory string) error {
attestationPolicyFile, err := os.ReadFile("../../scripts/attestation_policy/attestation_policy.json")
if err != nil {
return err
}
unmarshalOptions := protojson.UnmarshalOptions{DiscardUnknown: true}
err = unmarshalOptions.Unmarshal(attestationPolicyFile, policy)
if err != nil {
return err
}
policy.Config.Policy.Product = &sevsnp.SevProduct{Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN}
policy.Config.Policy.FamilyId = rr.Report.FamilyId
policy.Config.Policy.ImageId = rr.Report.ImageId
policy.Config.Policy.Measurement = rr.Report.Measurement
policy.Config.Policy.HostData = rr.Report.HostData
policy.Config.Policy.ReportIdMa = rr.Report.ReportIdMa
policy.Config.RootOfTrust.ProductLine = sevProductNameMilan
policyByte, err := vtpm.ConvertPolicyToJSON(&policy)
if err != nil {
return err
}
policyPath := filepath.Join(policyDirectory, "attestation_policy.json")
err = os.WriteFile(policyPath, policyByte, 0o644)
if err != nil {
return nil
}
attestation.AttestationPolicyPath = policyPath
return nil
}
-322
View File
@@ -1,322 +0,0 @@
#include "extensions.h"
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <openssl/x509.h>
#include <fcntl.h>
#include <unistd.h>
extern int callVerificationValidationCallback(int platformType, const u_char* pubKey, int pubKeyLen, const u_char* quote, int quoteSize, const u_char* teeNonceByte, const u_char* vTPMNonceByte);
extern u_char* callFetchAttestationCallback(int platformType, const u_char* pubKey, int pubKeyLen, const u_char* teeNonceByte, const u_char* vTPMNonceByte, unsigned long* outlen);
extern uintptr_t validationVerificationCallback(int teeType);
extern uintptr_t getPlatformTypeHandle(int platformType, u_char *teeNonce, u_char *vtpmNonce);
extern int returnCCPlatformType();
int triggerVerificationValidationCallback(int platformType, u_char* pub_key, int pub_key_len, u_char *quote, int quote_size, u_char *tee_nonce, u_char *vtpm_nonce) {
if (quote == NULL || vtpm_nonce == NULL || tee_nonce == NULL || pub_key == NULL) {
fprintf(stderr, "attestation and noce and public key cannot be NULL\n");
return -1;
}
return callVerificationValidationCallback(platformType, pub_key, pub_key_len, quote, quote_size, tee_nonce, vtpm_nonce);
}
u_char* triggerFetchAttestationCallback(int platformType, u_char* pub_key, int pub_key_len, char *tee_nonce, char *vtpm_nonce, unsigned long *outlen) {
if(tee_nonce == NULL || vtpm_nonce == NULL) {
fprintf(stderr, "Report data cannot be NULL");
return NULL;
}
return callFetchAttestationCallback(platformType, pub_key, pub_key_len, tee_nonce, vtpm_nonce, outlen);
}
/*
Evidence request extension
- Contains a random nonce that goes into the attestation report
- Is sent in the ClientHello message
*/
void evidence_request_ext_free_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char *out,
void *add_arg)
{
free((void *)out);
}
int evidence_request_ext_add_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char **out,
size_t *outlen, X509 *x,
size_t chainidx, int *al,
void *add_arg)
{
switch (context)
{
case SSL_EXT_CLIENT_HELLO:
{
tls_extension_data *ext_data = (tls_extension_data*)add_arg;
evidence_request *er = (evidence_request*)malloc(sizeof(evidence_request));
if (er == NULL) {
perror("could not allocate memory");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
if (ext_data != NULL) {
if (RAND_bytes(ext_data->er.vtpm_nonce, NONCE_RANDOM_SIZE) != 1) {
perror("could not generate random bytes for vtpm nonce, will use SSL client random");
SSL_get_client_random(s, ext_data->er.vtpm_nonce, NONCE_RANDOM_SIZE);
}
if (RAND_bytes(ext_data->er.tee_nonce, REPORT_DATA_SIZE) != 1) {
perror("could not generate random bytes for tee nonce, will use SSL client random");
SSL_get_client_random(s, ext_data->er.tee_nonce, REPORT_DATA_SIZE);
}
} else {
fprintf(stderr, "add_arg is NULL\n");
free(er);
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
memcpy(er->vtpm_nonce, ext_data->er.vtpm_nonce, NONCE_RANDOM_SIZE);
memcpy(er->tee_nonce, ext_data->er.tee_nonce, REPORT_DATA_SIZE);
*out = (const u_char *)er;
*outlen = sizeof(evidence_request);
return 1;
}
case SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS:
{
tls_extension_data *ext_data = (tls_extension_data*)add_arg;
if (ext_data != NULL) {
int32_t *platform_type = (int32_t*)malloc(sizeof(int32_t));
if (platform_type == NULL) {
perror("could not allocate memory");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
*platform_type = returnCCPlatformType();
ext_data->platform_type = *platform_type;
*out = (u_char*)platform_type;
*outlen = sizeof(int32_t);
} else {
fprintf(stderr, "add_arg is NULL\n");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
return 1;
}
default:
break;
}
fprintf(stderr, "bad context\n");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
int evidence_request_ext_parse_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char *in,
size_t inlen, X509 *x,
size_t chainidx, int *al,
void *parse_arg)
{
switch (context)
{
case SSL_EXT_CLIENT_HELLO:
{
tls_extension_data *ext_data = (tls_extension_data*)parse_arg;
evidence_request *er = (evidence_request*)in;
if (ext_data != NULL) {
memcpy(ext_data->er.vtpm_nonce, er->vtpm_nonce, NONCE_RANDOM_SIZE);
memcpy(ext_data->er.tee_nonce, er->tee_nonce, REPORT_DATA_SIZE);
} else {
fprintf(stderr, "parse_arg is NULL\n");
return 0;
}
return 1;
}
case SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS:
{
int *platform_type = (int*)in;
tls_extension_data *ext_data = (tls_extension_data*)parse_arg;
if (ext_data != NULL) {
ext_data->platform_type = *platform_type;
} else {
fprintf(stderr, "parse_arg is NULL\n");
return 0;
}
return 1;
}
default:
fprintf(stderr, "bad context\n");
return 0;
}
}
/*
Attestation Certificate extension
- Contains the attestation report
- The attestation report contains the hash of the nonce, the Public Key of the x.509 Agent certificate, and the vTPM AK
*/
void attestation_certificate_ext_free_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char *out,
void *add_arg)
{
free((void *)out);
}
int attestation_certificate_ext_add_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char **out,
size_t *outlen, X509 *x,
size_t chainidx, int *al,
void *add_arg)
{
switch (context)
{
case SSL_EXT_CLIENT_HELLO:
return 1;
case SSL_EXT_TLS1_3_CERTIFICATE:
{
tls_extension_data *ext_data = (tls_extension_data*)add_arg;
if (ext_data != NULL) {
u_char *quote;
size_t len = 0;
EVP_PKEY *pkey = NULL;
u_char *pubkey_buf = NULL;
int pubkey_len = 0;
if (x != NULL) {
pkey = X509_get_pubkey(x);
if (pkey == NULL) {
fprintf(stderr, "Failed to extract public key from certificate\n");
return -1;
}
pubkey_len = i2d_PUBKEY(pkey, &pubkey_buf);
if (pubkey_len <= 0) {
fprintf(stderr, "Failed to convert public key to DER format\n");
EVP_PKEY_free(pkey);
return -1;
}
} else {
fprintf(stderr, "agent certificate must be used for aTLS\n");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
quote = triggerFetchAttestationCallback(ext_data->platform_type, pubkey_buf, pubkey_len, ext_data->er.tee_nonce, ext_data->er.vtpm_nonce, &len);
if (quote == NULL) {
fprintf(stderr, "attestation report is NULL\n");
*al = SSL_AD_INTERNAL_ERROR;
EVP_PKEY_free(pkey);
OPENSSL_free(pubkey_buf);
return -1;
}
EVP_PKEY_free(pkey);
OPENSSL_free(pubkey_buf);
*out = quote;
*outlen = len;
return 1;
} else {
fprintf(stderr, "add_arg is NULL\n");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
}
default:
fprintf(stderr, "bad context\n");
*al = SSL_AD_INTERNAL_ERROR;
return -1;
}
}
int attestation_certificate_ext_parse_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const u_char *in,
size_t inlen, X509 *x,
size_t chainidx, int *al,
void *parse_arg)
{
switch (context)
{
case SSL_EXT_CLIENT_HELLO:
return 1;
case SSL_EXT_TLS1_3_CERTIFICATE:
{
if (x != NULL) {
tls_extension_data *ext_data = (tls_extension_data*)parse_arg;
if (ext_data != NULL) {
char *quote = (char*)malloc(inlen*sizeof(char));
EVP_PKEY *pkey = NULL;
u_char *pubkey_buf = NULL;
int pubkey_len = 0;
int res = 0;
if (quote == NULL) {
perror("could not allocate memory");
return 0;
}
pkey = X509_get_pubkey(x);
if (pkey == NULL) {
fprintf(stderr, "Failed to extract public key from certificate\n");
return -1;
}
pubkey_len = i2d_PUBKEY(pkey, &pubkey_buf);
if (pubkey_len <= 0) {
fprintf(stderr, "Failed to convert public key to DER format\n");
EVP_PKEY_free(pkey);
return -1;
}
memcpy(quote, in, inlen);
res = triggerVerificationValidationCallback(ext_data->platform_type,
pubkey_buf,
pubkey_len,
quote,
inlen,
(u_char*)&ext_data->er.tee_nonce,
(u_char*)&ext_data->er.vtpm_nonce);
free(quote);
EVP_PKEY_free(pkey);
OPENSSL_free(pubkey_buf);
if (res != 0) {
fprintf(stderr, "verification and validation failed, aborting connection\n");
return 0;
}
} else {
fprintf(stderr, "parse_arg is NULL\n");
return 0;
}
return 1;
} else {
fprintf(stderr, "agent certificates must be used for aTLS\n");
return 0;
}
}
default:
fprintf(stderr, "bad context\n");
return 0;
}
}
-92
View File
@@ -1,92 +0,0 @@
#ifndef ATLS_EXTENSION_H
#define ATLS_EXTENSION_H
#include <openssl/ssl.h>
#include <arpa/inet.h>
#define EVIDENCE_REQUEST_HELLO_EXTENSION_TYPE 65
#define ATTESTATION_CERTIFICATE_EXTENSION_TYPE 66
#define REPORT_DATA_SIZE 64
#define NONCE_RANDOM_SIZE 32
#define TLS_CLIENT_CTX 0
#define TLS_SERVER_CTX 1
typedef struct evidence_request
{
char vtpm_nonce[NONCE_RANDOM_SIZE];
char tee_nonce[REPORT_DATA_SIZE];
} evidence_request;
typedef struct tls_extension_data
{
int platform_type;
evidence_request er;
} tls_extension_data;
typedef struct tls_server_connection
{
int server_fd;
char* cert;
int cert_len;
char* key;
int key_len;
struct sockaddr_storage addr;
} tls_server_connection;
typedef struct tls_connection
{
SSL_CTX *ctx;
SSL *ssl;
int socket_fd;
struct sockaddr_storage local_addr;
struct sockaddr_storage remote_addr;
tls_extension_data tls_ext_data;
} tls_connection;
tls_server_connection* start_tls_server(const char* cert, int cert_len, const char* key, int key_len, const char* ip, int port);
tls_connection* tls_server_accept(tls_server_connection *tls_server);
int tls_server_close(tls_server_connection *tls_server);
int tls_read(tls_connection *conn, void *buf, int num);
int tls_write(tls_connection *conn, const void *buf, int num);
int tls_close(tls_connection *conn);
tls_connection* new_tls_connection(char *address, int port);
int set_socket_read_timeout(tls_connection* conn, int timeout_sec, int timeout_usec);
int set_socket_write_timeout(tls_connection* conn, int timeout_sec, int timeout_usec);
char* tls_return_addr(struct sockaddr_storage *addr);
int tls_return_port(struct sockaddr_storage *addr);
// Extensions
void evidence_request_ext_free_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char *out,
void *add_arg);
int evidence_request_ext_add_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char **out,
size_t *outlen, X509 *x,
size_t chainidx, int *al,
void *add_arg);
int evidence_request_ext_parse_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char *in,
size_t inlen, X509 *x,
size_t chainidx, int *al,
void *parse_arg);
void attestation_certificate_ext_free_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char *out,
void *add_arg);
int attestation_certificate_ext_add_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char **out,
size_t *outlen, X509 *x,
size_t chainidx, int *al,
void *add_arg);
int attestation_certificate_ext_parse_cb(SSL *s, unsigned int ext_type,
unsigned int context,
const unsigned char *in,
size_t inlen, X509 *x,
size_t chainidx, int *al,
void *parse_arg);
#endif // ATLS_EXTENSION_H
-607
View File
@@ -1,607 +0,0 @@
#include "extensions.h"
#include <openssl/err.h>
#include <openssl/x509.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <ifaddrs.h>
void init_openssl() {
SSL_load_error_strings();
OpenSSL_add_ssl_algorithms();
}
void cleanup_openssl() {
EVP_cleanup();
}
int load_certificates_from_memory(SSL_CTX* ctx, const char* cert, int cert_len, const char* key, int key_len) {
BIO* cert_bio = NULL;
BIO* key_bio = NULL;
X509* x509_cert = NULL;
EVP_PKEY* pkey = NULL;
int success = 0;
cert_bio = BIO_new_mem_buf(cert, cert_len);
if (cert_bio == NULL) {
goto cleanup;
}
key_bio = BIO_new_mem_buf(key, key_len);
if (key_bio == NULL) {
goto cleanup;
}
x509_cert = PEM_read_bio_X509(cert_bio, NULL, 0, NULL);
if (x509_cert == NULL) {
goto cleanup;
}
pkey = PEM_read_bio_PrivateKey(key_bio, NULL, 0, NULL);
if (pkey == NULL) {
goto cleanup;
}
if (SSL_CTX_use_certificate(ctx, x509_cert) <= 0) {
goto cleanup;
}
if (SSL_CTX_use_PrivateKey(ctx, pkey) <= 0) {
goto cleanup;
}
if (SSL_CTX_check_private_key(ctx) <= 0) {
goto cleanup;
}
success = 1;
cleanup:
if (cert_bio) BIO_free(cert_bio);
if (key_bio) BIO_free(key_bio);
if (x509_cert) X509_free(x509_cert);
if (pkey) EVP_PKEY_free(pkey);
if (!success) {
ERR_print_errors_fp(stderr);
}
return success;
}
int enforce_tls1_3_only(SSL_CTX *ctx) {
if (SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION) == 0) {
return 0;
}
if (SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) == 0) {
return 0;
}
return 1;
}
SSL_CTX *create_context(int is_server) {
const SSL_METHOD *method;
SSL_CTX *ctx;
if (is_server) {
method = TLS_server_method();
} else {
method = TLS_client_method();
}
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
return NULL;
}
if (!enforce_tls1_3_only(ctx)) {
fprintf(stderr, "could not enforce TLS1.3\n");
SSL_CTX_free(ctx);
return NULL;
}
return ctx;
}
int add_custom_tls_extension(SSL_CTX *ctx, tls_connection *conn) {
uint32_t flags_nonce = SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS;
uint32_t flags_attestation = SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE;
int ret = 1;
void *data = NULL;
if (conn != NULL) {
data = (void*)&conn->tls_ext_data;
}
ret = SSL_CTX_add_custom_ext(ctx,
EVIDENCE_REQUEST_HELLO_EXTENSION_TYPE,
flags_nonce,
evidence_request_ext_add_cb,
evidence_request_ext_free_cb,
data,
evidence_request_ext_parse_cb,
data);
if (ret != 1) {
return 0;
}
ret = SSL_CTX_add_custom_ext(ctx,
ATTESTATION_CERTIFICATE_EXTENSION_TYPE,
flags_attestation,
attestation_certificate_ext_add_cb,
attestation_certificate_ext_free_cb,
data,
attestation_certificate_ext_parse_cb,
data);
if (ret != 1) {
return 0;
}
return ret;
}
// Function to start the TLS server
tls_server_connection* start_tls_server(const char* cert, int cert_len, const char* key, int key_len, const char* ip, int port) {
tls_server_connection *tls_server = (tls_server_connection*)malloc(sizeof(tls_server_connection));
int opt = 0;
if (tls_server == NULL) {
perror("memory could not be allocated");
return NULL;
}
init_openssl();
tls_server->cert = (char*)malloc(cert_len * sizeof(char));
if (tls_server->cert == NULL) {
perror("memory could not be allocated");
goto cleanup_tls_server;
}
tls_server->key = (char*)malloc(key_len * sizeof(char));
if (tls_server->key == NULL) {
perror("memory could not be allocated");
goto cleanup_cert;
}
memcpy(tls_server->cert, cert, cert_len);
memcpy(tls_server->key, key, key_len);
tls_server->cert_len = cert_len;
tls_server->key_len = key_len;
tls_server->server_fd = socket(AF_INET6, SOCK_STREAM, 0);
if (tls_server->server_fd < 0) {
perror("Unable to create socket");
goto cleanup_key;
}
// Enable both IPv4-mapped and IPv6 addresses
if (setsockopt(tls_server->server_fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)) != 0) {
perror("setsockopt(IPV6_V6ONLY) failed");
goto cleanup_socket;
}
// Configure address structure
memset(&(tls_server->addr), 0, sizeof(tls_server->addr));
struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)&tls_server->addr;
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(port);
// Set the appropriate address (IPv4-mapped if needed)
if (ip == NULL || (strlen(ip) + 1) < INET_ADDRSTRLEN) {
addr6->sin6_addr = in6addr_any;
} else if (strchr(ip, ':') != NULL) {
if (inet_pton(AF_INET6, ip, &(addr6->sin6_addr)) <= 0) {
perror("Invalid IPv6 address");
goto cleanup_socket;
}
} else {
struct in_addr ipv4_addr;
if (inet_pton(AF_INET, ip, &ipv4_addr) <= 0) {
perror("Invalid IPv4 address");
goto cleanup_socket;
}
memset(&addr6->sin6_addr, 0, sizeof(addr6->sin6_addr));
addr6->sin6_addr.s6_addr[10] = 0xff;
addr6->sin6_addr.s6_addr[11] = 0xff;
memcpy(&addr6->sin6_addr.s6_addr[12], &ipv4_addr, sizeof(ipv4_addr));
}
if (bind(tls_server->server_fd, (struct sockaddr*)&(tls_server->addr), sizeof(tls_server->addr)) < 0) {
perror("Unable to bind");
goto cleanup_socket;
}
if (listen(tls_server->server_fd, SOMAXCONN) < 0) {
perror("Unable to listen");
goto cleanup_socket;
}
printf("Listening on port: %d\n", port);
return tls_server;
// Cleanup labels
cleanup_socket:
close(tls_server->server_fd);
cleanup_key:
free(tls_server->key);
cleanup_cert:
free(tls_server->cert);
cleanup_tls_server:
free(tls_server);
return NULL;
}
// Function to accept a client connection
tls_connection* tls_server_accept(tls_server_connection *tls_server) {
uint32_t len = sizeof(struct sockaddr_storage);
tls_connection *conn = (tls_connection*)malloc(sizeof(tls_connection));
int client_fd = -1;
int ret = 0;
if (conn == NULL) {
perror("Unable to allocate memory for tls_connection");
return NULL;
}
conn->ctx = NULL;
conn->ssl = NULL;
conn->socket_fd = -1;
// Initialize the context
conn->ctx = create_context(TLS_SERVER_CTX);
if (conn->ctx == NULL) {
perror("Unable to create context");
goto cleanup_conn;
}
// Load certificates
if (!load_certificates_from_memory(conn->ctx, tls_server->cert, tls_server->cert_len, tls_server->key, tls_server->key_len)) {
fprintf(stderr, "Failed to load certificates\n");
goto cleanup_ctx;
}
// Add custom TLS extension
ret = add_custom_tls_extension(conn->ctx, conn);
if (!ret) {
perror("Unable to add custom tls extensions");
goto cleanup_ctx;
}
// Accept client connection
client_fd = accept(tls_server->server_fd, NULL, NULL);
if (client_fd < 0) {
perror("Unable to accept connection");
goto cleanup_ctx;
}
// Create SSL object
conn->ssl = SSL_new(conn->ctx);
if (conn->ssl == NULL) {
perror("Unable to create SSL object");
goto cleanup_fd;
}
// Set file descriptor
conn->socket_fd = client_fd;
SSL_set_fd(conn->ssl, client_fd);
// Get local address
if (getsockname(client_fd, (struct sockaddr *)&conn->local_addr, &len) == -1) {
perror("getsockname failed during TLS server accept");
goto cleanup_ssl;
}
// Get remote address
if (getpeername(client_fd, (struct sockaddr *)&conn->remote_addr, &len) == -1) {
perror("getpeername failed during TLS server accept");
goto cleanup_ssl;
}
// Perform SSL handshake
ret = SSL_accept(conn->ssl);
if (ret <= 0) {
perror("SSL handshake failed during accept");
goto cleanup_ssl;
}
return conn;
cleanup_ssl:
if (conn->ssl) SSL_free(conn->ssl);
cleanup_fd:
if (client_fd >= 0) close(client_fd);
cleanup_ctx:
if (conn->ctx) SSL_CTX_free(conn->ctx);
cleanup_conn:
free(conn);
return NULL;
}
// Function to close the server
int tls_server_close(tls_server_connection *tls_server) {
close(tls_server->server_fd);
cleanup_openssl();
free(tls_server->cert);
free(tls_server->key);
free(tls_server);
return 0;
}
int tls_read(tls_connection *conn, void *buf, int num) {
return SSL_read(conn->ssl, buf, num);
}
int tls_write(tls_connection *conn, const void *buf, int num) {
if (SSL_get_shutdown(conn->ssl) & SSL_SENT_SHUTDOWN) {
return 0;
}
return SSL_write(conn->ssl, buf, num);
}
int tls_close(tls_connection *conn) {
if (conn != NULL) {
if (conn->ssl != NULL) {
int ret = 0;
if (SSL_has_pending(conn->ssl) == 1 || (SSL_get_shutdown(conn->ssl) & SSL_SENT_SHUTDOWN)) {
int num = SSL_pending(conn->ssl);
char c[num];
int res = 0;
int end = 0;
res = SSL_read(conn->ssl, (void*)c, num);
res = SSL_get_error(conn->ssl, res);
if (res == SSL_ERROR_ZERO_RETURN) {
end = 1;
} else if (res != SSL_ERROR_NONE) {
fprintf(stderr, "SSL_read failed in TLS close call\n");
end = 1;
}
if ((SSL_get_shutdown(conn->ssl) & SSL_RECEIVED_SHUTDOWN) || end == 1) {
ret = SSL_shutdown(conn->ssl);
}
} else {
ret = SSL_shutdown(conn->ssl);
}
if (ret < 0) {
ret = SSL_get_error(conn->ssl, ret);
fprintf(stderr, "SSL did not shutdown correctly, error code: %d\n", ret);
free(conn);
close(conn->socket_fd);
conn = NULL;
return -1;
} else if (ret == 0) {
return 0;
}
conn->ssl = NULL;
}
if (conn->socket_fd >= 0) {
close(conn->socket_fd);
conn->socket_fd = -1;
}
SSL_free(conn->ssl);
if (conn->ctx != NULL) {
SSL_CTX_free(conn->ctx);
conn->ctx = NULL;
}
free(conn);
conn = NULL;
return 1;
}
return 1;
}
char* tls_return_addr(struct sockaddr_storage *addr) {
socklen_t addr_len = sizeof(struct sockaddr_storage);
int inet_size =addr->ss_family == AF_INET ? INET_ADDRSTRLEN : INET6_ADDRSTRLEN;
char *ip_str = (char*)malloc(inet_size*sizeof(char));
void * addr_ptr;
if (ip_str == NULL) {
perror("memory could not be allocated");
return NULL;
}
if (addr->ss_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)addr;
addr_ptr = &(ipv4->sin_addr);
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)addr;
addr_ptr = &(ipv6->sin6_addr);
} else {
fprintf(stderr, "unknown family: %d\n", addr->ss_family);
free(ip_str);
return NULL;
}
if (inet_ntop(addr->ss_family, addr_ptr, ip_str, inet_size) == NULL) {
perror("inet_ntop failed");
free(ip_str);
return NULL;
}
return ip_str;
}
int tls_return_port(struct sockaddr_storage *addr) {
if (addr->ss_family == AF_INET) {
struct sockaddr_in *ipv4 = (struct sockaddr_in *)addr;
return ntohs(ipv4->sin_port);
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)addr;
return ntohs(ipv6->sin6_port);
}
fprintf(stderr, "cannot return port from unknown family: %d\n", addr->ss_family);
return -1;
}
tls_connection* new_tls_connection(char *address, int port) {
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
int socket_fd = -1;
int status;
struct addrinfo hints, *res = NULL, *p = NULL;
char port_str[6];
tls_connection *conn = NULL;
socklen_t addr_len;
int ret = 0;
conn = (tls_connection*)malloc(sizeof(tls_connection));
if (!conn) {
perror("Failed to allocate memory for atls connection");
return NULL;
}
// Format the port string
snprintf(port_str, sizeof(port_str), "%d", port);
// Initialize OpenSSL
init_openssl();
// Create SSL context
ctx = create_context(TLS_CLIENT_CTX);
if (!ctx) {
perror("Could not create context");
goto cleanup_conn;
}
conn->ctx = ctx;
// Add custom TLS extension
ret = add_custom_tls_extension(conn->ctx, conn);
if (!ret) {
perror("Unable to add custom tls extensions");
goto cleanup_ctx;
}
// Prepare the address info hints
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = 0;
hints.ai_protocol = 0;
// Get address info
status = getaddrinfo(address, port_str, &hints, &res);
if (status != 0) {
perror("getaddrinfo error");
goto cleanup_ctx;
}
// Iterate through the results and try to connect
for (p = res; p != NULL; p = p->ai_next) {
socket_fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (socket_fd < 0) {
perror("unable to create socket");
continue;
}
if (connect(socket_fd, p->ai_addr, p->ai_addrlen)) {
close(socket_fd);
continue;
}
memcpy(&conn->local_addr, p->ai_addr, p->ai_addrlen);
break;
}
freeaddrinfo(res);
if (p == NULL) {
goto cleanup_ctx;
}
conn->socket_fd = socket_fd;
// Retrieve and store the remote address
addr_len = sizeof(conn->remote_addr);
if (getpeername(socket_fd, (struct sockaddr *)&conn->remote_addr, &addr_len) == -1) {
perror("getpeername failed");
goto cleanup_socket;
}
// Create the SSL structure
ssl = SSL_new(ctx);
if (!ssl) {
perror("Failed to create SSL object");
goto cleanup_socket;
}
if (!SSL_set_fd(ssl, socket_fd)) {
perror("Failed to set SSL file descriptor");
goto cleanup_ssl;
}
conn->ssl = ssl;
// Perform the SSL handshake
if (SSL_connect(ssl) <= 0) {
fprintf(stderr, "SSL handshake failed\n");
goto cleanup_ssl;
}
return conn;
cleanup_ssl:
if (ssl) SSL_free(ssl);
cleanup_socket:
if (socket_fd >= 0) close(socket_fd);
cleanup_ctx:
if (ctx) SSL_CTX_free(ctx);
cleanup_conn:
free(conn);
return NULL;
}
int set_socket_timeout(tls_connection* conn, int timeout_sec, int timeout_usec) {
struct timeval timeout;
timeout.tv_sec = timeout_sec;
timeout.tv_usec = timeout_usec;
if (setsockopt(conn->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
return -1;
}
if (setsockopt(conn->socket_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) {
return -1;
}
return 0;
}
int set_socket_read_timeout(tls_connection* conn, int timeout_sec, int timeout_usec) {
struct timeval timeout;
timeout.tv_sec = timeout_sec;
timeout.tv_usec = timeout_usec;
if (setsockopt(conn->socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
return -1;
}
return 0;
}
int set_socket_write_timeout(tls_connection* conn, int timeout_sec, int timeout_usec) {
struct timeval timeout;
timeout.tv_sec = timeout_sec;
timeout.tv_usec = timeout_usec;
if (setsockopt(conn->socket_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) {
return -1;
}
return 0;
}
+32 -44
View File
@@ -4,17 +4,14 @@
package attestation
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/client"
"github.com/google/go-sev-guest/proto/check"
tdxcliet "github.com/google/go-tdx-guest/client"
"github.com/google/go-tpm/legacy/tpm2"
"google.golang.org/protobuf/encoding/protojson"
)
type PlatformType int
@@ -25,6 +22,7 @@ const (
SNPvTPM
AzureToken
Azure
TDX
NoCC
)
@@ -33,13 +31,7 @@ const (
azureApiVersion = "2021-02-01"
)
var (
AttestationPolicy = Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &PcrConfig{}}
ErrAttestationPolicyOpen = errors.New("failed to open Attestation Policy file")
ErrAttestationPolicyDecode = errors.New("failed to decode Attestation Policy file")
ErrAttestationPolicyMissing = errors.New("failed due to missing Attestation Policy file")
ErrAttestationPolicyEncode = errors.New("failed to encode the Attestation Policy")
)
var AttestationPolicyPath string
type PcrValues struct {
Sha256 map[string]string `json:"sha256"`
@@ -65,45 +57,23 @@ type Provider interface {
Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error)
TeeAttestation(teeNonce []byte) ([]byte, error)
VTpmAttestation(vTpmNonce []byte) ([]byte, error)
VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error
VerifTeeAttestation(report []byte, teeNonce []byte) error
VerifVTpmAttestation(report []byte, vTpmNonce []byte) error
AzureAttestationToken(tokenNonce []byte) ([]byte, error)
}
func ReadAttestationPolicy(policyPath string, attestationConfiguration *Config) error {
if policyPath != "" {
policyData, err := os.ReadFile(policyPath)
if err != nil {
return errors.Wrap(ErrAttestationPolicyOpen, err)
}
return ReadAttestationPolicyFromByte(policyData, attestationConfiguration)
}
return ErrAttestationPolicyMissing
}
func ReadAttestationPolicyFromByte(policyData []byte, attestationConfiguration *Config) error {
unmarshalOptions := protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
if err := unmarshalOptions.Unmarshal(policyData, attestationConfiguration.Config); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
if err := json.Unmarshal(policyData, attestationConfiguration.PcrConfig); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
return nil
type Verifier interface {
VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error
VerifTeeAttestation(report []byte, teeNonce []byte) error
VerifVTpmAttestation(report []byte, vTpmNonce []byte) error
JSONToPolicy(path string) error
}
// CCPlatform returns the type of the confidential computing platform.
func CCPlatform() PlatformType {
checks := []ccCheck{
{SevGuestvTPMExists, SNPvTPM},
{SevGuesDeviceExists, SNP},
{SevSnpGuestvTPMExists, SNPvTPM},
{SevSnpGuestDeviceExists, SNP},
{isAzureVM, Azure},
{TDXGuestDeviceExists, TDX},
}
for _, c := range checks {
@@ -114,7 +84,7 @@ func CCPlatform() PlatformType {
return NoCC
}
func SevGuesDeviceExists() bool {
func SevSnpGuestDeviceExists() bool {
d, err := client.OpenDevice()
if err != nil {
return false
@@ -124,17 +94,25 @@ func SevGuesDeviceExists() bool {
return true
}
func SevGuestvTPMExists() bool {
func SevSnpGuestvTPMExists() bool {
return vTPMExists() && SevSnpGuestDeviceExists()
}
func vTPMExists() bool {
d, err := tpm2.OpenTPM()
if err != nil {
return false
}
d.Close()
return SevGuesDeviceExists()
return true
}
func isAzureVM() bool {
if !vTPMExists() {
return false
}
client := &http.Client{}
url := fmt.Sprintf("%s?api-version=%s", azureMetadataUrl, azureApiVersion)
@@ -158,3 +136,13 @@ func isAzureVM() bool {
return false
}
func TDXGuestDeviceExists() bool {
d, err := tdxcliet.OpenDevice()
if err != nil {
return false
}
d.Close()
return true
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package attestation
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCCPlatform(t *testing.T) {
tests := []struct {
name string
sevSnpGuestExists bool
sevSnpGuestvTPMExists bool
tdxGuestExists bool
isAzure bool
expected PlatformType
}{
{
name: "No CC platform detected",
sevSnpGuestExists: false,
sevSnpGuestvTPMExists: false,
tdxGuestExists: false,
isAzure: false,
expected: NoCC,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CCPlatform()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSevSnpGuestDeviceExists(t *testing.T) {
tests := []struct {
name string
openDeviceErr error
expected bool
}{
{
name: "device does not exist or fails to open",
openDeviceErr: fmt.Errorf("device not found"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SevSnpGuestDeviceExists()
assert.Equal(t, tt.expected, result)
})
}
}
func TestSevSnpGuestvTPMExists(t *testing.T) {
tests := []struct {
name string
vTPMExists bool
sevSnpExists bool
expected bool
}{
{
name: "vTPM exists but SEV-SNP does not",
vTPMExists: true,
sevSnpExists: false,
expected: false,
},
{
name: "SEV-SNP exists but vTPM does not",
vTPMExists: false,
sevSnpExists: true,
expected: false,
},
{
name: "neither exists",
vTPMExists: false,
sevSnpExists: false,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := SevSnpGuestvTPMExists()
assert.Equal(t, tt.expected, result)
})
}
}
func TestVTPMExists(t *testing.T) {
tests := []struct {
name string
openTPMErr error
expected bool
}{
{
name: "TPM fails to open",
openTPMErr: fmt.Errorf("TPM not found"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := vTPMExists()
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsAzureVM(t *testing.T) {
tests := []struct {
name string
vTPMExists bool
statusCode int
responseBody string
httpError error
expected bool
}{
{
name: "Azure VM with empty response body",
vTPMExists: true,
statusCode: http.StatusOK,
responseBody: "",
httpError: nil,
expected: false,
},
{
name: "Azure VM with non-200 status code",
vTPMExists: true,
statusCode: http.StatusNotFound,
responseBody: "",
httpError: nil,
expected: false,
},
{
name: "HTTP request error",
vTPMExists: true,
statusCode: 0,
responseBody: "",
httpError: fmt.Errorf("connection failed"),
expected: false,
},
{
name: "vTPM does not exist",
vTPMExists: false,
statusCode: http.StatusOK,
responseBody: `{"compute":{"name":"test-vm"}}`,
httpError: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "GET", r.Method)
assert.Equal(t, "true", r.Header.Get("Metadata"))
expectedURL := fmt.Sprintf("/?api-version=%s", azureApiVersion)
assert.Equal(t, expectedURL, r.URL.String())
if tt.httpError != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(tt.statusCode)
if tt.responseBody != "" {
if _, err := w.Write([]byte(tt.responseBody)); err != nil {
t.Fatalf("Failed to write response body: %v", err)
}
}
}))
defer server.Close()
if tt.httpError != nil {
server.Close()
}
result := isAzureVM()
assert.Equal(t, tt.expected, result)
})
}
}
func TestTDXGuestDeviceExists(t *testing.T) {
tests := []struct {
name string
openDeviceErr error
expected bool
}{
{
name: "TDX device does not exist or fails to open",
openDeviceErr: fmt.Errorf("device not found"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := TDXGuestDeviceExists()
assert.Equal(t, tt.expected, result)
})
}
}
+57 -22
View File
@@ -25,16 +25,20 @@ import (
"google.golang.org/protobuf/proto"
)
var MaaURL = "https://sharedeus2.eus2.attest.azure.net"
var (
MaaURL = "https://sharedeus2.eus2.attest.azure.net"
ErrFetchAzureToken = errors.New("failed to fetch Azure token")
)
var _ attestation.Provider = (*provider)(nil)
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
type provider struct {
writer io.Writer
}
type provider struct{}
func New(writer io.Writer) attestation.Provider {
return provider{writer: writer}
func NewProvider() attestation.Provider {
return provider{}
}
func (a provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
@@ -84,20 +88,56 @@ func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return proto.Marshal(quote)
}
func (a provider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
quote, err := FetchAzureAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return quote, nil
}
type verifier struct {
writer io.Writer
Policy *attestation.Config
}
func NewVerifier(writer io.Writer) attestation.Verifier {
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
return verifier{
writer: writer,
Policy: policy,
}
}
func NewVerifierWithPolicy(writer io.Writer, policy *attestation.Config) attestation.Verifier {
if policy == nil {
return NewVerifier(writer)
}
return verifier{
writer: writer,
Policy: policy,
}
}
func (a verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
attestationReport, err := abi.ReportCertsToProto(report)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to convert TEE report to proto"), err)
}
return quoteprovider.VerifyAttestationReportTLS(attestationReport, teeNonce)
return quoteprovider.VerifyAttestationReportTLS(attestationReport, teeNonce, a.Policy)
}
func (a provider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return vtpm.VerifyQuote(report, nil, vTpmNonce, a.writer)
func (a verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return vtpm.VerifyQuote(report, vTpmNonce, a.writer, a.Policy)
}
func (a provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
func (a verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
@@ -108,20 +148,15 @@ func (a provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []
}
snpReport := quote.GetSevSnpAttestation()
if err = quoteprovider.VerifyAttestationReportTLS(snpReport, nil); err != nil {
if err = quoteprovider.VerifyAttestationReportTLS(snpReport, nil, a.Policy); err != nil {
return fmt.Errorf("failed to verify vTPM attestation report: %w", err)
}
return nil
}
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
quote, err := FetchAzureAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(vtpm.ErrFetchAzureToken, err)
}
return quote, nil
func (a verifier) JSONToPolicy(path string) error {
return vtpm.ReadPolicy(path, a.Policy)
}
func GenerateAttestationPolicy(token, product string, policy uint64) (*attestation.Config, error) {
@@ -219,7 +254,7 @@ func GenerateAttestationPolicy(token, product string, policy uint64) (*attestati
return nil, fmt.Errorf("failed to decode reportID: %w", err)
}
sevProduct := quoteprovider.GetProductName(product)
sevSnpProduct := quoteprovider.GetProductName(product)
return &attestation.Config{
Config: &check.Config{
@@ -233,7 +268,7 @@ func GenerateAttestationPolicy(token, product string, policy uint64) (*attestati
MinimumGuestSvn: uint32(guestSVN),
TrustedIdKeyHashes: [][]byte{idKeyDigest},
ReportId: reportID,
Product: &sevsnp.SevProduct{Name: sevProduct},
Product: &sevsnp.SevProduct{Name: sevSnpProduct},
Policy: policy,
},
},
+578
View File
@@ -0,0 +1,578 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/stretchr/testify/assert"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/proto"
)
var (
testNonce = []byte("test-nonce-12345678901234567890123456789012")
testReport = []byte("test-report-data")
)
func TestNewProvider(t *testing.T) {
tests := []struct {
name string
want attestation.Provider
}{
{
name: "creates new provider successfully",
want: provider{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewProvider()
assert.Equal(t, tt.want, got)
})
}
}
func TestProvider_Attestation(t *testing.T) {
tests := []struct {
name string
teeNonce []byte
vTpmNonce []byte
wantErr bool
errorMessage string
}{
{
name: "maa parameters error",
teeNonce: testNonce,
vTpmNonce: testNonce,
wantErr: true,
errorMessage: "failed to get report",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewProvider()
result, err := p.Attestation(tt.teeNonce, tt.vTpmNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func TestProvider_TeeAttestation(t *testing.T) {
tests := []struct {
name string
teeNonce []byte
wantErr bool
errorMessage string
}{
{
name: "maa parameters error",
teeNonce: testNonce,
wantErr: true,
errorMessage: "failed to get report",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewProvider()
result, err := p.TeeAttestation(tt.teeNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func TestProvider_AzureAttestationToken(t *testing.T) {
tests := []struct {
name string
tokenNonce []byte
setupServer func() *httptest.Server
wantErr bool
errorMessage string
}{
{
name: "server error",
tokenNonce: testNonce,
setupServer: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
},
wantErr: true,
errorMessage: "failed to fetch Azure token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := tt.setupServer()
defer server.Close()
originalURL := MaaURL
MaaURL = server.URL
defer func() { MaaURL = originalURL }()
p := NewProvider()
result, err := p.AzureAttestationToken(tt.tokenNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func TestNewVerifier(t *testing.T) {
tests := []struct {
name string
writer io.Writer
}{
{
name: "creates verifier with buffer writer",
writer: &bytes.Buffer{},
},
{
name: "creates verifier with nil writer",
writer: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := NewVerifier(tt.writer)
verifier, ok := v.(verifier)
assert.True(t, ok)
assert.Equal(t, tt.writer, verifier.writer)
assert.NotNil(t, verifier.Policy)
assert.NotNil(t, verifier.Policy.Config)
assert.NotNil(t, verifier.Policy.PcrConfig)
})
}
}
func TestNewVerifierWithPolicy(t *testing.T) {
tests := []struct {
name string
writer io.Writer
policy *attestation.Config
}{
{
name: "creates verifier with custom policy",
writer: &bytes.Buffer{},
policy: &attestation.Config{
Config: &check.Config{
Policy: &check.Policy{},
RootOfTrust: &check.RootOfTrust{},
},
PcrConfig: &attestation.PcrConfig{},
},
},
{
name: "creates verifier with nil policy",
writer: &bytes.Buffer{},
policy: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := NewVerifierWithPolicy(tt.writer, tt.policy)
verifier, ok := v.(verifier)
assert.True(t, ok)
assert.Equal(t, tt.writer, verifier.writer)
assert.NotNil(t, verifier.Policy)
})
}
}
func TestVerifier_VerifTeeAttestation(t *testing.T) {
tests := []struct {
name string
report []byte
teeNonce []byte
wantErr bool
errorMessage string
}{
{
name: "empty report",
report: []byte{},
teeNonce: testNonce,
wantErr: true,
},
{
name: "invalid report format",
report: []byte("invalid-report"),
teeNonce: testNonce,
wantErr: true,
},
{
name: "nil nonce",
report: testReport,
teeNonce: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := NewVerifier(&bytes.Buffer{})
err := v.VerifTeeAttestation(tt.report, tt.teeNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifier_VerifyAttestation(t *testing.T) {
validQuote := &attest.Attestation{
TeeAttestation: &attest.Attestation_SevSnpAttestation{
SevSnpAttestation: &sevsnp.Attestation{
Report: &sevsnp.Report{
HostData: []byte("test-data"),
},
Product: &sevsnp.SevProduct{
Name: sevsnp.SevProduct_SEV_PRODUCT_GENOA,
},
CertificateChain: &sevsnp.CertificateChain{
Extras: make(map[string][]byte),
},
},
},
}
validReport, _ := proto.Marshal(validQuote)
tests := []struct {
name string
report []byte
teeNonce []byte
vTpmNonce []byte
wantErr bool
errorMessage string
}{
{
name: "successful verification",
report: validReport,
teeNonce: testNonce,
vTpmNonce: testNonce,
wantErr: true,
errorMessage: "failed to verify vTPM attestation report",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := NewVerifier(&bytes.Buffer{})
err := v.VerifyAttestation(tt.report, tt.teeNonce, tt.vTpmNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestFetchAzureAttestationToken(t *testing.T) {
tests := []struct {
name string
tokenNonce []byte
maaURL string
setupServer func() *httptest.Server
wantErr bool
errorMessage string
}{
{
name: "server error",
tokenNonce: testNonce,
setupServer: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
},
wantErr: true,
errorMessage: "error fetching azure token",
},
{
name: "invalid url",
tokenNonce: testNonce,
setupServer: func() *httptest.Server {
return nil
},
wantErr: true,
errorMessage: "error fetching azure token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.setupServer != nil {
server := tt.setupServer()
if server != nil {
defer server.Close()
url = server.URL
}
}
if tt.name == "invalid url" {
url = "invalid-url"
}
result, err := FetchAzureAttestationToken(tt.tokenNonce, url)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func TestValidateToken(t *testing.T) {
tests := []struct {
name string
token string
setupServer func() *httptest.Server
wantErr bool
errorMessage string
}{
{
name: "invalid token format",
token: "invalid-token",
setupServer: nil,
wantErr: true,
errorMessage: "failed to parse token",
},
{
name: "empty token",
token: "",
setupServer: nil,
wantErr: true,
errorMessage: "failed to parse token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setupServer != nil {
server := tt.setupServer()
defer server.Close()
originalURL := MaaURL
MaaURL = server.URL
defer func() { MaaURL = originalURL }()
}
result, err := validateToken(tt.token)
if tt.wantErr {
assert.Error(t, err)
if tt.errorMessage != "" {
assert.Contains(t, err.Error(), tt.errorMessage)
}
assert.Nil(t, result)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
}
})
}
}
func TestIntegration_FullAttestationFlow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
t.Run("full attestation flow with mock server", func(t *testing.T) {
maaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/attest":
response := map[string]interface{}{
"token": createMockJWT(),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
t.Fatalf("Failed to encode response: %v", err)
}
case "/.well-known/openid_configuration":
config := map[string]interface{}{
"jwks_uri": "maaServer.URL" + "/certs",
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(config); err != nil {
t.Fatalf("Failed to encode OpenID configuration: %v", err)
}
case "/certs":
jwks := map[string]interface{}{
"keys": []map[string]interface{}{
{
"kid": "test-kid",
"kty": "RSA",
"use": "sig",
"n": "test-n-value",
"e": "AQAB",
},
},
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(jwks); err != nil {
t.Fatalf("Failed to encode JWKS: %v", err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer maaServer.Close()
originalURL := MaaURL
MaaURL = maaServer.URL
defer func() { MaaURL = originalURL }()
provider := NewProvider()
verifier := NewVerifier(&bytes.Buffer{})
teeNonce := []byte("test-tee-nonce-1234567890123456789012")
vtpmNonce := []byte("test-vtpm-nonce-123456789012345678901")
teeReport, err := provider.TeeAttestation(teeNonce)
if err != nil {
t.Logf("TEE attestation failed (expected in mock environment): %v", err)
}
vtpmReport, err := provider.VTpmAttestation(vtpmNonce)
if err != nil {
t.Logf("vTPM attestation failed (expected in mock environment): %v", err)
}
token, err := provider.AzureAttestationToken(teeNonce)
if err != nil {
t.Logf("Azure attestation token failed (expected in mock environment): %v", err)
}
assert.NotNil(t, provider)
assert.NotNil(t, verifier)
t.Logf("TEE report length: %d", len(teeReport))
t.Logf("vTPM report length: %d", len(vtpmReport))
t.Logf("Token length: %d", len(token))
})
}
func TestIntegration_ErrorPropagation(t *testing.T) {
t.Run("error propagation through full stack", func(t *testing.T) {
failingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
if _, err := w.Write([]byte("Internal Server Error")); err != nil {
t.Fatalf("Failed to write response: %v", err)
}
}))
defer failingServer.Close()
originalURL := MaaURL
MaaURL = failingServer.URL
defer func() { MaaURL = originalURL }()
provider := NewProvider()
_, err := provider.AzureAttestationToken([]byte("test-nonce"))
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch Azure token")
_, err = GenerateAttestationPolicy("invalid-token", "test-product", 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to validate token")
})
}
func createMockJWT() string {
claims := jwt.MapClaims{
"iss": "https://test-issuer.com",
"aud": "test-audience",
"exp": time.Now().Add(time.Hour).Unix(),
"iat": time.Now().Unix(),
"x-ms-isolation-tee": map[string]interface{}{
"x-ms-sevsnpvm-familyId": "1234567890abcdef",
"x-ms-sevsnpvm-imageId": "fedcba0987654321",
"x-ms-sevsnpvm-launchmeasurement": "abcdef1234567890",
"x-ms-sevsnpvm-bootloader-svn": float64(1),
"x-ms-sevsnpvm-tee-svn": float64(2),
"x-ms-sevsnpvm-snpfw-svn": float64(3),
"x-ms-sevsnpvm-microcode-svn": float64(4),
"x-ms-sevsnpvm-guestsvn": float64(5),
"x-ms-sevsnpvm-idkeydigest": "1234567890abcdef",
"x-ms-sevsnpvm-reportid": "fedcba0987654321",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["jku"] = "https://test-url.com"
token.Header["kid"] = "test-kid"
// Return unsigned token for testing
return token.Raw
}
@@ -0,0 +1,138 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery v2.53.3. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// MeasurementProvider is an autogenerated mock type for the MeasurementProvider type
type MeasurementProvider struct {
mock.Mock
}
type MeasurementProvider_Expecter struct {
mock *mock.Mock
}
func (_m *MeasurementProvider) EXPECT() *MeasurementProvider_Expecter {
return &MeasurementProvider_Expecter{mock: &_m.Mock}
}
// Run provides a mock function with given fields: binaryPath
func (_m *MeasurementProvider) Run(binaryPath string) ([]byte, error) {
ret := _m.Called(binaryPath)
if len(ret) == 0 {
panic("no return value specified for Run")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func(string) ([]byte, error)); ok {
return rf(binaryPath)
}
if rf, ok := ret.Get(0).(func(string) []byte); ok {
r0 = rf(binaryPath)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(binaryPath)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MeasurementProvider_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type MeasurementProvider_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
// - binaryPath string
func (_e *MeasurementProvider_Expecter) Run(binaryPath interface{}) *MeasurementProvider_Run_Call {
return &MeasurementProvider_Run_Call{Call: _e.mock.On("Run", binaryPath)}
}
func (_c *MeasurementProvider_Run_Call) Run(run func(binaryPath string)) *MeasurementProvider_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MeasurementProvider_Run_Call) Return(_a0 []byte, _a1 error) *MeasurementProvider_Run_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MeasurementProvider_Run_Call) RunAndReturn(run func(string) ([]byte, error)) *MeasurementProvider_Run_Call {
_c.Call.Return(run)
return _c
}
// Stop provides a mock function with no fields
func (_m *MeasurementProvider) Stop() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Stop")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// MeasurementProvider_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop'
type MeasurementProvider_Stop_Call struct {
*mock.Call
}
// Stop is a helper method to define mock.On call
func (_e *MeasurementProvider_Expecter) Stop() *MeasurementProvider_Stop_Call {
return &MeasurementProvider_Stop_Call{Call: _e.mock.On("Stop")}
}
func (_c *MeasurementProvider_Stop_Call) Run(run func()) *MeasurementProvider_Stop_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MeasurementProvider_Stop_Call) Return(_a0 error) *MeasurementProvider_Stop_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MeasurementProvider_Stop_Call) RunAndReturn(run func() error) *MeasurementProvider_Stop_Call {
_c.Call.Return(run)
return _c
}
// NewMeasurementProvider creates a new instance of MeasurementProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMeasurementProvider(t interface {
mock.TestingT
Cleanup(func())
}) *MeasurementProvider {
mock := &MeasurementProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
-12
View File
@@ -24,15 +24,3 @@ func (e *EmptyProvider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
func (e *EmptyProvider) AzureAttestationToken(nonce []byte) ([]byte, error) {
return nil, nil
}
func (e *EmptyProvider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
return nil
}
func (e *EmptyProvider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return nil
}
func (e *EmptyProvider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return nil
}
+261
View File
@@ -0,0 +1,261 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gcp
import (
"context"
"errors"
"testing"
"cloud.google.com/go/storage"
"github.com/google/gce-tcb-verifier/proto/endorsement"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/proto"
)
func TestExtract384BitMeasurement(t *testing.T) {
tests := []struct {
name string
attestation *sevsnp.Attestation
setupMock func()
expected string
expectError bool
errorMsg string
}{
{
name: "nil attestation",
attestation: nil,
expectError: true,
errorMsg: "report is nil",
},
{
name: "short report",
attestation: &sevsnp.Attestation{Report: &sevsnp.Report{}},
expectError: true,
errorMsg: "failed to transform report to binary",
},
{
name: "empty report",
attestation: &sevsnp.Attestation{},
expectError: true,
errorMsg: "failed to transform report to binary",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Extract384BitMeasurement(tt.attestation)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
assert.Empty(t, result)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
func TestGetLaunchEndorsement(t *testing.T) {
tests := []struct {
name string
measurement384 string
setupMock func() ([]byte, error)
expectError bool
errorMsg string
}{
{
name: "successful retrieval",
measurement384: "test-measurement",
setupMock: func() ([]byte, error) {
goldenUEFI := &endorsement.VMGoldenMeasurement{
SevSnp: &endorsement.VMSevSnp{
Policy: 12345,
Measurements: map[uint32][]byte{1: []byte("test-measurement")},
},
}
goldenBytes, _ := proto.Marshal(goldenUEFI)
launchEndorsement := &endorsement.VMLaunchEndorsement{
SerializedUefiGolden: goldenBytes,
}
return proto.Marshal(launchEndorsement)
},
expectError: false,
},
{
name: "storage client error",
measurement384: "test-measurement",
setupMock: func() ([]byte, error) {
return nil, errors.New("storage client error")
},
expectError: true,
errorMsg: "failed to create reader",
},
{
name: "object not found",
measurement384: "non-existent-measurement",
setupMock: func() ([]byte, error) {
return nil, storage.ErrObjectNotExist
},
expectError: true,
errorMsg: "failed to create reader",
},
{
name: "invalid protobuf data",
measurement384: "test-measurement",
setupMock: func() ([]byte, error) {
return []byte("invalid protobuf data"), nil
},
expectError: true,
errorMsg: "failed to create reader",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
// skip if credentials are not set
if _, err := storage.NewClient(ctx); err != nil && tt.expectError {
t.Skip("Skipping test due to missing GCP credentials")
}
_, err := GetLaunchEndorsement(ctx, tt.measurement384)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
}
})
}
}
func TestGenerateAttestationPolicy(t *testing.T) {
tests := []struct {
name string
endorsement *endorsement.VMGoldenMeasurement
vcpuNum uint32
expectError bool
errorMsg string
}{
{
name: "valid endorsement",
endorsement: &endorsement.VMGoldenMeasurement{
SevSnp: &endorsement.VMSevSnp{
Policy: 12345,
Measurements: map[uint32][]byte{1: []byte("test-measurement")},
},
},
vcpuNum: 1,
expectError: false,
},
{
name: "missing measurement for vcpu",
endorsement: &endorsement.VMGoldenMeasurement{
SevSnp: &endorsement.VMSevSnp{
Policy: 12345,
Measurements: map[uint32][]byte{2: []byte("test-measurement")},
},
},
vcpuNum: 1,
expectError: false,
},
{
name: "empty measurements map",
endorsement: &endorsement.VMGoldenMeasurement{
SevSnp: &endorsement.VMSevSnp{
Policy: 12345,
Measurements: map[uint32][]byte{},
},
},
vcpuNum: 1,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GenerateAttestationPolicy(tt.endorsement, tt.vcpuNum)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotNil(t, result.Config)
assert.NotNil(t, result.Config.Policy)
assert.NotNil(t, result.Config.RootOfTrust)
assert.NotNil(t, result.PcrConfig)
assert.Equal(t, tt.endorsement.SevSnp.Policy, result.Config.Policy.Policy)
assert.Equal(t, tt.endorsement.SevSnp.Measurements[tt.vcpuNum], result.Config.Policy.Measurement)
assert.False(t, result.Config.RootOfTrust.DisallowNetwork)
assert.True(t, result.Config.RootOfTrust.CheckCrl)
assert.Equal(t, "Milan", result.Config.RootOfTrust.Product)
assert.Equal(t, "Milan", result.Config.RootOfTrust.ProductLine)
}
})
}
}
func TestDownloadOvmfFile(t *testing.T) {
tests := []struct {
name string
digest string
expectError bool
errorMsg string
}{
{
name: "successful download",
digest: "test-digest",
expectError: false,
},
{
name: "storage client error",
digest: "test-digest",
expectError: true,
errorMsg: "failed to create reader",
},
{
name: "object not found",
digest: "non-existent-digest",
expectError: true,
errorMsg: "failed to create reader",
},
{
name: "read error",
digest: "test-digest",
expectError: true,
errorMsg: "failed to create reader",
},
{
name: "empty digest",
digest: "",
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
// skip if credentials are not set
if _, err := storage.NewClient(ctx); err != nil && tt.expectError {
t.Skip("Skipping test due to missing GCP credentials")
}
_, err := DownloadOvmfFile(ctx, tt.digest)
if tt.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
}
})
}
}
-142
View File
@@ -253,148 +253,6 @@ func (_c *Provider_VTpmAttestation_Call) RunAndReturn(run func([]byte) ([]byte,
return _c
}
// VerifTeeAttestation provides a mock function with given fields: report, teeNonce
func (_m *Provider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
ret := _m.Called(report, teeNonce)
if len(ret) == 0 {
panic("no return value specified for VerifTeeAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
r0 = rf(report, teeNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Provider_VerifTeeAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifTeeAttestation'
type Provider_VerifTeeAttestation_Call struct {
*mock.Call
}
// VerifTeeAttestation is a helper method to define mock.On call
// - report []byte
// - teeNonce []byte
func (_e *Provider_Expecter) VerifTeeAttestation(report interface{}, teeNonce interface{}) *Provider_VerifTeeAttestation_Call {
return &Provider_VerifTeeAttestation_Call{Call: _e.mock.On("VerifTeeAttestation", report, teeNonce)}
}
func (_c *Provider_VerifTeeAttestation_Call) Run(run func(report []byte, teeNonce []byte)) *Provider_VerifTeeAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte))
})
return _c
}
func (_c *Provider_VerifTeeAttestation_Call) Return(_a0 error) *Provider_VerifTeeAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Provider_VerifTeeAttestation_Call) RunAndReturn(run func([]byte, []byte) error) *Provider_VerifTeeAttestation_Call {
_c.Call.Return(run)
return _c
}
// VerifVTpmAttestation provides a mock function with given fields: report, vTpmNonce
func (_m *Provider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
ret := _m.Called(report, vTpmNonce)
if len(ret) == 0 {
panic("no return value specified for VerifVTpmAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
r0 = rf(report, vTpmNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Provider_VerifVTpmAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifVTpmAttestation'
type Provider_VerifVTpmAttestation_Call struct {
*mock.Call
}
// VerifVTpmAttestation is a helper method to define mock.On call
// - report []byte
// - vTpmNonce []byte
func (_e *Provider_Expecter) VerifVTpmAttestation(report interface{}, vTpmNonce interface{}) *Provider_VerifVTpmAttestation_Call {
return &Provider_VerifVTpmAttestation_Call{Call: _e.mock.On("VerifVTpmAttestation", report, vTpmNonce)}
}
func (_c *Provider_VerifVTpmAttestation_Call) Run(run func(report []byte, vTpmNonce []byte)) *Provider_VerifVTpmAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte))
})
return _c
}
func (_c *Provider_VerifVTpmAttestation_Call) Return(_a0 error) *Provider_VerifVTpmAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Provider_VerifVTpmAttestation_Call) RunAndReturn(run func([]byte, []byte) error) *Provider_VerifVTpmAttestation_Call {
_c.Call.Return(run)
return _c
}
// VerifyAttestation provides a mock function with given fields: report, teeNonce, vTpmNonce
func (_m *Provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
ret := _m.Called(report, teeNonce, vTpmNonce)
if len(ret) == 0 {
panic("no return value specified for VerifyAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte, []byte) error); ok {
r0 = rf(report, teeNonce, vTpmNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Provider_VerifyAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyAttestation'
type Provider_VerifyAttestation_Call struct {
*mock.Call
}
// VerifyAttestation is a helper method to define mock.On call
// - report []byte
// - teeNonce []byte
// - vTpmNonce []byte
func (_e *Provider_Expecter) VerifyAttestation(report interface{}, teeNonce interface{}, vTpmNonce interface{}) *Provider_VerifyAttestation_Call {
return &Provider_VerifyAttestation_Call{Call: _e.mock.On("VerifyAttestation", report, teeNonce, vTpmNonce)}
}
func (_c *Provider_VerifyAttestation_Call) Run(run func(report []byte, teeNonce []byte, vTpmNonce []byte)) *Provider_VerifyAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte), args[2].([]byte))
})
return _c
}
func (_c *Provider_VerifyAttestation_Call) Return(_a0 error) *Provider_VerifyAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Provider_VerifyAttestation_Call) RunAndReturn(run func([]byte, []byte, []byte) error) *Provider_VerifyAttestation_Call {
_c.Call.Return(run)
return _c
}
// NewProvider creates a new instance of Provider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewProvider(t interface {
+223
View File
@@ -0,0 +1,223 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
// Code generated by mockery v2.53.3. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// Verifier is an autogenerated mock type for the Verifier type
type Verifier struct {
mock.Mock
}
type Verifier_Expecter struct {
mock *mock.Mock
}
func (_m *Verifier) EXPECT() *Verifier_Expecter {
return &Verifier_Expecter{mock: &_m.Mock}
}
// JSONToPolicy provides a mock function with given fields: path
func (_m *Verifier) JSONToPolicy(path string) error {
ret := _m.Called(path)
if len(ret) == 0 {
panic("no return value specified for JSONToPolicy")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(path)
} else {
r0 = ret.Error(0)
}
return r0
}
// Verifier_JSONToPolicy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JSONToPolicy'
type Verifier_JSONToPolicy_Call struct {
*mock.Call
}
// JSONToPolicy is a helper method to define mock.On call
// - path string
func (_e *Verifier_Expecter) JSONToPolicy(path interface{}) *Verifier_JSONToPolicy_Call {
return &Verifier_JSONToPolicy_Call{Call: _e.mock.On("JSONToPolicy", path)}
}
func (_c *Verifier_JSONToPolicy_Call) Run(run func(path string)) *Verifier_JSONToPolicy_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *Verifier_JSONToPolicy_Call) Return(_a0 error) *Verifier_JSONToPolicy_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Verifier_JSONToPolicy_Call) RunAndReturn(run func(string) error) *Verifier_JSONToPolicy_Call {
_c.Call.Return(run)
return _c
}
// VerifTeeAttestation provides a mock function with given fields: report, teeNonce
func (_m *Verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
ret := _m.Called(report, teeNonce)
if len(ret) == 0 {
panic("no return value specified for VerifTeeAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
r0 = rf(report, teeNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Verifier_VerifTeeAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifTeeAttestation'
type Verifier_VerifTeeAttestation_Call struct {
*mock.Call
}
// VerifTeeAttestation is a helper method to define mock.On call
// - report []byte
// - teeNonce []byte
func (_e *Verifier_Expecter) VerifTeeAttestation(report interface{}, teeNonce interface{}) *Verifier_VerifTeeAttestation_Call {
return &Verifier_VerifTeeAttestation_Call{Call: _e.mock.On("VerifTeeAttestation", report, teeNonce)}
}
func (_c *Verifier_VerifTeeAttestation_Call) Run(run func(report []byte, teeNonce []byte)) *Verifier_VerifTeeAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte))
})
return _c
}
func (_c *Verifier_VerifTeeAttestation_Call) Return(_a0 error) *Verifier_VerifTeeAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Verifier_VerifTeeAttestation_Call) RunAndReturn(run func([]byte, []byte) error) *Verifier_VerifTeeAttestation_Call {
_c.Call.Return(run)
return _c
}
// VerifVTpmAttestation provides a mock function with given fields: report, vTpmNonce
func (_m *Verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
ret := _m.Called(report, vTpmNonce)
if len(ret) == 0 {
panic("no return value specified for VerifVTpmAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
r0 = rf(report, vTpmNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Verifier_VerifVTpmAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifVTpmAttestation'
type Verifier_VerifVTpmAttestation_Call struct {
*mock.Call
}
// VerifVTpmAttestation is a helper method to define mock.On call
// - report []byte
// - vTpmNonce []byte
func (_e *Verifier_Expecter) VerifVTpmAttestation(report interface{}, vTpmNonce interface{}) *Verifier_VerifVTpmAttestation_Call {
return &Verifier_VerifVTpmAttestation_Call{Call: _e.mock.On("VerifVTpmAttestation", report, vTpmNonce)}
}
func (_c *Verifier_VerifVTpmAttestation_Call) Run(run func(report []byte, vTpmNonce []byte)) *Verifier_VerifVTpmAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte))
})
return _c
}
func (_c *Verifier_VerifVTpmAttestation_Call) Return(_a0 error) *Verifier_VerifVTpmAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Verifier_VerifVTpmAttestation_Call) RunAndReturn(run func([]byte, []byte) error) *Verifier_VerifVTpmAttestation_Call {
_c.Call.Return(run)
return _c
}
// VerifyAttestation provides a mock function with given fields: report, teeNonce, vTpmNonce
func (_m *Verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
ret := _m.Called(report, teeNonce, vTpmNonce)
if len(ret) == 0 {
panic("no return value specified for VerifyAttestation")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte, []byte) error); ok {
r0 = rf(report, teeNonce, vTpmNonce)
} else {
r0 = ret.Error(0)
}
return r0
}
// Verifier_VerifyAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VerifyAttestation'
type Verifier_VerifyAttestation_Call struct {
*mock.Call
}
// VerifyAttestation is a helper method to define mock.On call
// - report []byte
// - teeNonce []byte
// - vTpmNonce []byte
func (_e *Verifier_Expecter) VerifyAttestation(report interface{}, teeNonce interface{}, vTpmNonce interface{}) *Verifier_VerifyAttestation_Call {
return &Verifier_VerifyAttestation_Call{Call: _e.mock.On("VerifyAttestation", report, teeNonce, vTpmNonce)}
}
func (_c *Verifier_VerifyAttestation_Call) Run(run func(report []byte, teeNonce []byte, vTpmNonce []byte)) *Verifier_VerifyAttestation_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].([]byte), args[2].([]byte))
})
return _c
}
func (_c *Verifier_VerifyAttestation_Call) Return(_a0 error) *Verifier_VerifyAttestation_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Verifier_VerifyAttestation_Call) RunAndReturn(run func([]byte, []byte, []byte) error) *Verifier_VerifyAttestation_Call {
_c.Call.Return(run)
return _c
}
// NewVerifier creates a new instance of Verifier. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewVerifier(t interface {
mock.TestingT
Cleanup(func())
}) *Verifier {
mock := &Verifier{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+15 -24
View File
@@ -22,15 +22,14 @@ import (
"github.com/google/go-sev-guest/verify/trust"
"github.com/google/logger"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/proto"
)
const (
cocosDirectory = ".cocos"
caBundleName = "ask_ark.pem"
Nonce = 64
sevProductNameMilan = "Milan"
sevProductNameGenoa = "Genoa"
cocosDirectory = ".cocos"
caBundleName = "ask_ark.pem"
Nonce = 64
sevSnpProductMilan = "Milan"
sevSnpProductGenoa = "Genoa"
)
var (
@@ -39,8 +38,8 @@ var (
)
var (
errProductLine = errors.New(fmt.Sprintf("product name must be %s or %s", sevProductNameMilan, sevProductNameGenoa))
errAttVerification = errors.New("attestation verification failed")
ErrProductLine = errors.New(fmt.Sprintf("product name must be %s or %s", sevSnpProductMilan, sevSnpProductGenoa))
ErrAttVerification = errors.New("attestation verification failed")
errAttValidation = errors.New("attestation validation failed")
)
@@ -73,21 +72,16 @@ func fillInAttestationLocal(attestation *sevsnp.Attestation, cfg *check.Config)
return nil
}
func copyConfig(attConf *check.Config) (*check.Config, error) {
copy := proto.Clone(attConf).(*check.Config)
return copy, nil
}
func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
sopts, err := verify.RootOfTrustToOptions(cfg.RootOfTrust)
if err != nil {
return fmt.Errorf("failed to get root of trust options: %v", errors.Wrap(errAttVerification, err))
return fmt.Errorf("failed to get root of trust options: %v", errors.Wrap(ErrAttVerification, err))
}
if cfg.Policy.Product == nil {
productName := GetProductName(cfg.RootOfTrust.ProductLine)
if productName == sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN {
return errProductLine
return ErrProductLine
}
sopts.Product = &sevsnp.SevProduct{
@@ -108,7 +102,7 @@ func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
}
if err := verify.SnpAttestation(attestationPB, sopts); err != nil {
return errors.Wrap(errAttVerification, err)
return errors.Wrap(ErrAttVerification, err)
}
return nil
@@ -117,7 +111,7 @@ func verifyReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
func validateReport(attestationPB *sevsnp.Attestation, cfg *check.Config) error {
opts, err := validate.PolicyToOptions(cfg.Policy)
if err != nil {
return fmt.Errorf("failed to get policy for validation: %v", errors.Wrap(errAttVerification, err))
return fmt.Errorf("failed to get policy for validation: %v", errors.Wrap(ErrAttVerification, err))
}
if err = validate.SnpAttestation(attestationPB, opts); err != nil {
@@ -131,11 +125,8 @@ func GetLeveledQuoteProvider() (client.LeveledQuoteProvider, error) {
return client.GetLeveledQuoteProvider()
}
func VerifyAttestationReportTLS(attestationPB *sevsnp.Attestation, reportData []byte) error {
config, err := copyConfig(attestation.AttestationPolicy.Config)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to create a copy of attestation policy"), err)
}
func VerifyAttestationReportTLS(attestationPB *sevsnp.Attestation, reportData []byte, policy *attestation.Config) error {
config := policy.Config
// Certificate chain is populated based on the extra data that is appended to the SEV-SNP attestation report.
// This data is not part of the attestation report and it will be ignored.
@@ -185,9 +176,9 @@ func FetchAttestation(reportDataSlice []byte, vmpl uint) ([]byte, error) {
func GetProductName(product string) sevsnp.SevProduct_SevProductName {
switch product {
case sevProductNameMilan:
case sevSnpProductMilan:
return sevsnp.SevProduct_SEV_PRODUCT_MILAN
case sevProductNameGenoa:
case sevSnpProductGenoa:
return sevsnp.SevProduct_SEV_PRODUCT_GENOA
default:
return sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN
+275 -111
View File
@@ -7,208 +7,372 @@
package quoteprovider
import (
"fmt"
"os"
"path"
"testing"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
config "github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
func TestFillInAttestationLocal(t *testing.T) {
originalHome := os.Getenv("HOME")
defer func() {
os.Setenv("HOME", originalHome)
}()
tempDir, err := os.MkdirTemp("", "test_home")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
cocosDir := tempDir + "/.cocos/Milan"
os.Setenv("HOME", tempDir)
cocosDir := path.Join(tempDir, cocosDirectory, sevSnpProductMilan)
err = os.MkdirAll(cocosDir, 0o755)
require.NoError(t, err)
bundleContent := []byte("mock ASK ARK bundle")
err = os.WriteFile(cocosDir+"/ask_ark.pem", bundleContent, 0o644)
bundlePath := path.Join(cocosDir, caBundleName)
err = os.WriteFile(bundlePath, bundleContent, 0o644)
require.NoError(t, err)
config := check.Config{
RootOfTrust: &check.RootOfTrust{},
Policy: &check.Policy{},
config := &check.Config{
RootOfTrust: &check.RootOfTrust{
ProductLine: sevSnpProductMilan,
},
Policy: &check.Policy{},
}
tests := []struct {
name string
attestation *sevsnp.Attestation
err error
name string
attestation *sevsnp.Attestation
setupFunc func()
expectedError bool
errorContains string
}{
{
name: "Empty attestation",
name: "Empty attestation - creates new chain",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
CertificateChain: nil,
},
err: nil,
setupFunc: func() {},
expectedError: true,
errorContains: "could not find ASK or ASVK PEM block; could not find ARK PEM block",
},
{
name: "Attestation with existing chain",
name: "Attestation with existing chain - no changes needed",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{
AskCert: []byte("existing ASK cert"),
ArkCert: []byte("existing ARK cert"),
},
},
err: nil,
setupFunc: func() {},
expectedError: false,
},
{
name: "Attestation with empty chain - tries to load from file",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
},
setupFunc: func() {},
expectedError: true,
errorContains: "could not find ASK or ASVK PEM block; could not find ARK PEM block",
},
{
name: "No bundle file exists - no error",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
},
setupFunc: func() {
os.Remove(bundlePath)
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := fillInAttestationLocal(tt.attestation, &config)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
os.Setenv("HOME", tempDir)
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
if err := os.WriteFile(bundlePath, bundleContent, 0o644); err != nil {
t.Fatalf("Failed to write bundle file: %v", err)
}
}
tt.setupFunc()
err := fillInAttestationLocal(tt.attestation, config)
if tt.expectedError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifyAttestationReportSuccess(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
func TestGetProductName(t *testing.T) {
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
goodProduct int
err error
name string
product string
expected sevsnp.SevProduct_SevProductName
}{
{
name: "Valid attestation, validation and verification is performed succsessfully",
attestationReport: attestationPB,
reportData: reportData,
goodProduct: 1,
err: nil,
name: "Milan product",
product: sevSnpProductMilan,
expected: sevsnp.SevProduct_SEV_PRODUCT_MILAN,
},
{
name: "Genoa product",
product: sevSnpProductGenoa,
expected: sevsnp.SevProduct_SEV_PRODUCT_GENOA,
},
{
name: "Unknown product",
product: "UnknownProduct",
expected: sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN,
},
{
name: "Empty product",
product: "",
expected: sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN,
},
{
name: "Case sensitive - milan lowercase",
product: "milan",
expected: sevsnp.SevProduct_SEV_PRODUCT_UNKNOWN,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
result := GetProductName(tt.product)
assert.Equal(t, tt.expected, result)
})
}
}
func TestVerifyAttestationReportMalformedSignature(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
// Change random data so in the signature so the signature failes
attestationPB.Report.Signature[0] = attestationPB.Report.Signature[0] ^ 0x01
func TestVerifyReport(t *testing.T) {
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
name string
attestation *sevsnp.Attestation
config *check.Config
expectedError bool
errorContains string
}{
{
name: "Valid attestation, distorted signature",
attestationReport: attestationPB,
reportData: reportData,
err: errAttVerification,
name: "Invalid product line",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
},
config: &check.Config{
RootOfTrust: &check.RootOfTrust{
ProductLine: "InvalidProduct",
},
Policy: &check.Policy{},
},
expectedError: true,
errorContains: "product name must be",
},
{
name: "Valid Milan product line",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{
AskCert: []byte("mock ask cert"),
ArkCert: []byte("mock ark cert"),
},
},
config: &check.Config{
RootOfTrust: &check.RootOfTrust{
ProductLine: sevSnpProductMilan,
},
Policy: &check.Policy{},
},
expectedError: true,
errorContains: "attestation verification failed",
},
{
name: "Valid Genoa product line",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{
AskCert: []byte("mock ask cert"),
ArkCert: []byte("mock ark cert"),
},
},
config: &check.Config{
RootOfTrust: &check.RootOfTrust{
ProductLine: sevSnpProductGenoa,
},
Policy: &check.Policy{},
},
expectedError: true,
errorContains: "attestation verification failed",
},
{
name: "Config with existing product policy",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{
AskCert: []byte("mock ask cert"),
ArkCert: []byte("mock ark cert"),
},
},
config: &check.Config{
RootOfTrust: &check.RootOfTrust{
ProductLine: sevSnpProductMilan,
},
Policy: &check.Policy{
Product: &sevsnp.SevProduct{
Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN,
},
},
},
expectedError: true,
errorContains: "attestation verification failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
err := verifyReport(tt.attestation, tt.config)
if tt.expectedError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifyAttestationReportUnknownProduct(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
func TestValidateReport(t *testing.T) {
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
name string
attestation *sevsnp.Attestation
config *check.Config
expectedError bool
errorContains string
}{
{
name: "Valid attestation, unknown product",
attestationReport: attestationPB,
reportData: reportData,
err: errProductLine,
name: "Basic validation test",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
},
config: &check.Config{
Policy: &check.Policy{
Policy: 196608,
},
},
expectedError: true,
errorContains: "attestation validation failed",
},
{
name: "Validation with report data",
attestation: &sevsnp.Attestation{
CertificateChain: &sevsnp.CertificateChain{},
},
config: &check.Config{
Policy: &check.Policy{
Policy: 196608,
ReportData: []byte("test report datatest report datatest report datatest report data"),
},
},
expectedError: true,
errorContains: "attestation validation failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config.AttestationPolicy.Config.RootOfTrust.ProductLine = ""
config.AttestationPolicy.Config.Policy.Product = nil
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
err := validateReport(tt.attestation, tt.config)
if tt.expectedError {
assert.Error(t, err)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifyAttestationReportMalformedPolicy(t *testing.T) {
attestationPB, reportData := prepVerifyAttReport(t)
// Change random data in the measurement so the measurement does not match
attestationPB.Report.Measurement[0] = attestationPB.Report.Measurement[0] ^ 0x01
func TestFetchAttestation(t *testing.T) {
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
name string
reportData []byte
vmpl uint
expectedError bool
errorContains string
}{
{
name: "Valid attestation, malformed policy (measurement)",
attestationReport: attestationPB,
reportData: reportData,
err: errAttVerification,
name: "Report data too large",
reportData: make([]byte, Nonce+1),
vmpl: 0,
expectedError: true,
errorContains: "could not get quote provider",
},
{
name: "Valid report data size",
reportData: make([]byte, 32),
vmpl: 0,
expectedError: true,
errorContains: "could not get quote provider",
},
{
name: "Maximum valid report data size",
reportData: make([]byte, Nonce),
vmpl: 1,
expectedError: true,
errorContains: "could not get quote provider",
},
{
name: "Empty report data",
reportData: []byte{},
vmpl: 0,
expectedError: true,
errorContains: "could not get quote provider",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyAttestationReportTLS(tt.attestationReport, tt.reportData)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
result, err := FetchAttestation(tt.reportData, tt.vmpl)
if tt.expectedError {
assert.Error(t, err)
assert.Empty(t, result)
if tt.errorContains != "" {
assert.Contains(t, err.Error(), tt.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotEmpty(t, result)
}
})
}
}
func prepVerifyAttReport(t *testing.T) (*sevsnp.Attestation, []byte) {
file, err := os.ReadFile("../../../attestation.bin")
require.NoError(t, err)
func TestGetLeveledQuoteProvider(t *testing.T) {
t.Run("GetLeveledQuoteProvider call", func(t *testing.T) {
provider, err := GetLeveledQuoteProvider()
if len(file) < abi.ReportSize {
file = append(file, make([]byte, abi.ReportSize-len(file))...)
}
rr, err := abi.ReportCertsToProto(file)
require.NoError(t, err)
config.AttestationPolicy = config.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &config.PcrConfig{}}
attestationPolicyFile, err := os.ReadFile("../../../scripts/attestation_policy/attestation_policy.json")
require.NoError(t, err)
unmarshalOptions := protojson.UnmarshalOptions{DiscardUnknown: true}
err = unmarshalOptions.Unmarshal(attestationPolicyFile, config.AttestationPolicy.Config)
require.NoError(t, err)
config.AttestationPolicy.Config.Policy.Product = &sevsnp.SevProduct{Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN}
config.AttestationPolicy.Config.Policy.FamilyId = rr.Report.FamilyId
config.AttestationPolicy.Config.Policy.ImageId = rr.Report.ImageId
config.AttestationPolicy.Config.Policy.Measurement = rr.Report.Measurement
config.AttestationPolicy.Config.Policy.HostData = rr.Report.HostData
config.AttestationPolicy.Config.Policy.ReportIdMa = rr.Report.ReportIdMa
config.AttestationPolicy.Config.RootOfTrust.ProductLine = sevProductNameMilan
return rr, rr.Report.ReportData
if err != nil {
assert.Error(t, err)
assert.Nil(t, provider)
} else {
assert.NoError(t, err)
assert.NotNil(t, provider)
}
})
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
//go:build !embed
// +build !embed
package tdx
import (
"fmt"
"os"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-tdx-guest/abi"
"github.com/google/go-tdx-guest/client"
"github.com/google/go-tdx-guest/proto/checkconfig"
valdatetdx "github.com/google/go-tdx-guest/validate"
verifytdx "github.com/google/go-tdx-guest/verify"
trusttdx "github.com/google/go-tdx-guest/verify/trust"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
var errOpenTDXDevice = errors.New("failed to open TDX device")
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
var (
timeout = time.Minute * 2
maxTryDelay = time.Second * 30
)
type provider struct{}
func NewProvider() attestation.Provider {
return provider{}
}
func (v provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
return v.TeeAttestation(teeNonce)
}
func (v provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
if teeNonce == nil {
return nil, errors.New("tee nonce is required for TDX attestation")
}
if len(teeNonce) != 64 {
return nil, fmt.Errorf("invalid tee nonce length: expected 64 bytes, got %d bytes", len(teeNonce))
}
quoteprovider, err := client.GetQuoteProvider()
if err != nil {
return nil, errors.Wrap(err, errOpenTDXDevice)
}
return quoteprovider.GetRawQuote([64]byte(teeNonce))
}
func (v provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return nil, errors.New("vTPM attestation fetch is not supported")
}
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
}
type verifier struct {
Policy *checkconfig.Config
}
func NewVerifier() attestation.Verifier {
Policy := &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{HeaderPolicy: &checkconfig.HeaderPolicy{}, TdQuoteBodyPolicy: &checkconfig.TDQuoteBodyPolicy{}},
}
return verifier{
Policy: Policy,
}
}
func NewVerifierWithPolicy(policy *checkconfig.Config) attestation.Verifier {
if policy == nil {
return NewVerifier()
}
return verifier{
Policy: policy,
}
}
func (v verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
if v.Policy == nil {
return fmt.Errorf("tdx policy is not provided")
}
quote, err := abi.QuoteToProto(report)
if err != nil {
return err
}
sopts, err := verifytdx.RootOfTrustToOptions(v.Policy.RootOfTrust)
if err != nil {
return err
}
sopts.Getter = &trusttdx.RetryHTTPSGetter{
Timeout: timeout,
MaxRetryDelay: maxTryDelay,
Getter: &trusttdx.SimpleHTTPSGetter{},
}
if err := verifytdx.TdxQuote(quote, sopts); err != nil {
return err
}
opts, err := valdatetdx.PolicyToOptions(v.Policy.Policy)
if err != nil {
return err
}
if err := valdatetdx.TdxQuote(quote, opts); err != nil {
return err
}
return nil
}
func (v verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return errors.New("VTPM attestation verification is not supported")
}
func (v verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return v.VerifTeeAttestation(report, teeNonce)
}
func (v verifier) JSONToPolicy(path string) error {
return ReadTDXAttestationPolicy(path, v.Policy)
}
func ReadTDXAttestationPolicy(policyPath string, policy *checkconfig.Config) error {
policyByte, err := os.ReadFile(policyPath)
if err != nil {
return err
}
if err := protojson.Unmarshal(policyByte, policy); err != nil {
return err
}
return nil
}
+622
View File
@@ -0,0 +1,622 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package tdx
import (
"os"
"path/filepath"
"testing"
"github.com/google/go-tdx-guest/proto/checkconfig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/encoding/protojson"
)
func TestNewProvider(t *testing.T) {
tests := []struct {
name string
want attestation.Provider
}{
{
name: "should create new provider successfully",
want: provider{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewProvider()
assert.IsType(t, tt.want, got)
})
}
}
func TestProvider_Attestation(t *testing.T) {
tests := []struct {
name string
teeNonce []byte
vTpmNonce []byte
wantErr bool
errContains string
}{
{
name: "should handle empty nonces",
teeNonce: []byte{},
vTpmNonce: []byte{},
wantErr: true,
errContains: "invalid tee nonce length: expected 64 bytes, got 0 bytes",
},
{
name: "should handle valid nonces",
teeNonce: []byte("test-noncetest-noncetest-noncetest-noncetest-noncetest-noncetest"),
vTpmNonce: []byte("vtpm-nonce"),
wantErr: true,
errContains: "/sys/kernel/config/tsm/report",
},
{
name: "should handle nil nonces",
teeNonce: nil,
vTpmNonce: nil,
wantErr: true,
errContains: "tee nonce is required for TDX attestation",
},
{
name: "should handle large nonce",
teeNonce: make([]byte, 64),
vTpmNonce: make([]byte, 32),
wantErr: true,
errContains: "/sys/kernel/config/tsm/report",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := provider{}
got, err := p.Attestation(tt.teeNonce, tt.vTpmNonce)
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
assert.Nil(t, got)
} else {
assert.NoError(t, err)
assert.NotNil(t, got)
}
})
}
}
func TestProvider_TeeAttestation(t *testing.T) {
tests := []struct {
name string
teeNonce []byte
wantErr bool
errContains string
}{
{
name: "should handle empty nonce",
teeNonce: []byte{},
wantErr: true,
errContains: "invalid tee nonce length: expected 64 bytes, got 0 bytes",
},
{
name: "should handle valid nonce",
teeNonce: []byte("test-noncetest-noncetest-noncetest-noncetest-noncetest-noncetest"),
wantErr: true,
errContains: "/sys/kernel/config/tsm/report",
},
{
name: "should handle nil nonce",
teeNonce: nil,
wantErr: true,
errContains: "tee nonce is required for TDX attestation",
},
{
name: "should handle 64-byte nonce",
teeNonce: make([]byte, 64),
wantErr: true,
errContains: "/sys/kernel/config/tsm/report",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := provider{}
got, err := p.TeeAttestation(tt.teeNonce)
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
assert.Nil(t, got)
} else {
assert.NoError(t, err)
assert.NotNil(t, got)
}
})
}
}
func TestProvider_VTpmAttestation(t *testing.T) {
tests := []struct {
name string
vTpmNonce []byte
wantErr bool
errContains string
}{
{
name: "should return error for empty nonce",
vTpmNonce: []byte{},
wantErr: true,
errContains: "vTPM attestation fetch is not supported",
},
{
name: "should return error for valid nonce",
vTpmNonce: []byte("vtpm-nonce"),
wantErr: true,
errContains: "vTPM attestation fetch is not supported",
},
{
name: "should return error for nil nonce",
vTpmNonce: nil,
wantErr: true,
errContains: "vTPM attestation fetch is not supported",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := provider{}
got, err := p.VTpmAttestation(tt.vTpmNonce)
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
assert.Nil(t, got)
})
}
}
func TestProvider_AzureAttestationToken(t *testing.T) {
tests := []struct {
name string
tokenNonce []byte
wantErr bool
errContains string
}{
{
name: "should return error for empty nonce",
tokenNonce: []byte{},
wantErr: true,
errContains: "Azure attestation token is not supported",
},
{
name: "should return error for valid nonce",
tokenNonce: []byte("token-nonce"),
wantErr: true,
errContains: "Azure attestation token is not supported",
},
{
name: "should return error for nil nonce",
tokenNonce: nil,
wantErr: true,
errContains: "Azure attestation token is not supported",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := provider{}
got, err := p.AzureAttestationToken(tt.tokenNonce)
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
assert.Nil(t, got)
})
}
}
func TestNewVerifier(t *testing.T) {
tests := []struct {
name string
}{
{
name: "should create new verifier successfully",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewVerifier()
v, ok := got.(verifier)
assert.True(t, ok)
assert.NotNil(t, v.Policy)
assert.NotNil(t, v.Policy.RootOfTrust)
assert.NotNil(t, v.Policy.Policy)
assert.NotNil(t, v.Policy.Policy.HeaderPolicy)
assert.NotNil(t, v.Policy.Policy.TdQuoteBodyPolicy)
})
}
}
func TestNewVerifierWithPolicy(t *testing.T) {
tests := []struct {
name string
policy *checkconfig.Config
}{
{
name: "should create verifier with nil policy",
policy: nil,
},
{
name: "should create verifier with valid policy",
policy: &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{},
},
},
{
name: "should create verifier with empty policy",
policy: &checkconfig.Config{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewVerifierWithPolicy(tt.policy)
v, ok := got.(verifier)
assert.True(t, ok)
if tt.policy == nil {
assert.NotNil(t, v.Policy)
assert.NotNil(t, v.Policy.RootOfTrust)
assert.NotNil(t, v.Policy.Policy)
} else {
assert.Equal(t, tt.policy, v.Policy)
}
})
}
}
func TestVerifier_VerifTeeAttestation(t *testing.T) {
tests := []struct {
name string
verifier verifier
report []byte
teeNonce []byte
wantErr bool
errContains string
}{
{
name: "should return error when policy is nil",
verifier: verifier{
Policy: nil,
},
report: []byte("test-report"),
teeNonce: []byte("test-nonce"),
wantErr: true,
errContains: "tdx policy is not provided",
},
{
name: "should handle invalid report format",
verifier: verifier{
Policy: &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{},
},
},
report: []byte("invalid-report"),
teeNonce: []byte("test-nonce"),
wantErr: true,
errContains: "",
},
{
name: "should handle empty report",
verifier: verifier{
Policy: &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{},
},
},
report: []byte{},
teeNonce: []byte("test-nonce"),
wantErr: true,
errContains: "",
},
{
name: "should handle nil report",
verifier: verifier{
Policy: &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{},
},
},
report: nil,
teeNonce: []byte("test-nonce"),
wantErr: true,
errContains: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.verifier.VerifTeeAttestation(tt.report, tt.teeNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifier_VerifVTpmAttestation(t *testing.T) {
tests := []struct {
name string
verifier verifier
report []byte
vTpmNonce []byte
wantErr bool
errContains string
}{
{
name: "should return error for any input",
verifier: verifier{},
report: []byte("test-report"),
vTpmNonce: []byte("test-nonce"),
wantErr: true,
errContains: "VTPM attestation verification is not supported",
},
{
name: "should return error for empty inputs",
verifier: verifier{},
report: []byte{},
vTpmNonce: []byte{},
wantErr: true,
errContains: "VTPM attestation verification is not supported",
},
{
name: "should return error for nil inputs",
verifier: verifier{},
report: nil,
vTpmNonce: nil,
wantErr: true,
errContains: "VTPM attestation verification is not supported",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.verifier.VerifVTpmAttestation(tt.report, tt.vTpmNonce)
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
})
}
}
func TestVerifier_VerifyAttestation(t *testing.T) {
tests := []struct {
name string
verifier verifier
report []byte
teeNonce []byte
vTpmNonce []byte
wantErr bool
errContains string
}{
{
name: "should delegate to VerifTeeAttestation with nil policy",
verifier: verifier{
Policy: nil,
},
report: []byte("test-report"),
teeNonce: []byte("test-nonce"),
vTpmNonce: []byte("vtpm-nonce"),
wantErr: true,
errContains: "tdx policy is not provided",
},
{
name: "should delegate to VerifTeeAttestation with valid policy",
verifier: verifier{
Policy: &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{},
},
},
report: []byte("invalid-report"),
teeNonce: []byte("test-nonce"),
vTpmNonce: []byte("vtpm-nonce"),
wantErr: true,
errContains: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.verifier.VerifyAttestation(tt.report, tt.teeNonce, tt.vTpmNonce)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifier_JSONToPolicy(t *testing.T) {
tempDir := t.TempDir()
testPolicy := &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{
HeaderPolicy: &checkconfig.HeaderPolicy{},
TdQuoteBodyPolicy: &checkconfig.TDQuoteBodyPolicy{},
},
}
validPolicyJSON, err := protojson.Marshal(testPolicy)
require.NoError(t, err)
validPolicyFile := filepath.Join(tempDir, "valid_policy.json")
err = os.WriteFile(validPolicyFile, validPolicyJSON, 0o644)
require.NoError(t, err)
invalidPolicyFile := filepath.Join(tempDir, "invalid_policy.json")
err = os.WriteFile(invalidPolicyFile, []byte("invalid json"), 0o644)
require.NoError(t, err)
tests := []struct {
name string
verifier verifier
path string
wantErr bool
errContains string
}{
{
name: "should load valid policy file",
verifier: verifier{
Policy: &checkconfig.Config{},
},
path: validPolicyFile,
wantErr: false,
},
{
name: "should return error for non-existent file",
verifier: verifier{
Policy: &checkconfig.Config{},
},
path: filepath.Join(tempDir, "non_existent.json"),
wantErr: true,
errContains: "no such file or directory",
},
{
name: "should return error for invalid JSON",
verifier: verifier{
Policy: &checkconfig.Config{},
},
path: invalidPolicyFile,
wantErr: true,
errContains: "",
},
{
name: "should return error for empty path",
verifier: verifier{
Policy: &checkconfig.Config{},
},
path: "",
wantErr: true,
errContains: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.verifier.JSONToPolicy(tt.path)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestReadTDXAttestationPolicy(t *testing.T) {
tempDir := t.TempDir()
testPolicy := &checkconfig.Config{
RootOfTrust: &checkconfig.RootOfTrust{},
Policy: &checkconfig.Policy{
HeaderPolicy: &checkconfig.HeaderPolicy{},
TdQuoteBodyPolicy: &checkconfig.TDQuoteBodyPolicy{},
},
}
validPolicyJSON, err := protojson.Marshal(testPolicy)
require.NoError(t, err)
validPolicyFile := filepath.Join(tempDir, "valid_policy.json")
err = os.WriteFile(validPolicyFile, validPolicyJSON, 0o644)
require.NoError(t, err)
invalidPolicyFile := filepath.Join(tempDir, "invalid_policy.json")
err = os.WriteFile(invalidPolicyFile, []byte("invalid json"), 0o644)
require.NoError(t, err)
emptyFile := filepath.Join(tempDir, "empty.json")
err = os.WriteFile(emptyFile, []byte{}, 0o644)
require.NoError(t, err)
tests := []struct {
name string
policyPath string
policy *checkconfig.Config
wantErr bool
errContains string
}{
{
name: "should read valid policy file",
policyPath: validPolicyFile,
policy: &checkconfig.Config{},
wantErr: false,
},
{
name: "should return error for non-existent file",
policyPath: filepath.Join(tempDir, "non_existent.json"),
policy: &checkconfig.Config{},
wantErr: true,
errContains: "no such file or directory",
},
{
name: "should return error for invalid JSON",
policyPath: invalidPolicyFile,
policy: &checkconfig.Config{},
wantErr: true,
errContains: "",
},
{
name: "should return error for empty file",
policyPath: emptyFile,
policy: &checkconfig.Config{},
wantErr: true,
errContains: "",
},
{
name: "should return error for empty path",
policyPath: "",
policy: &checkconfig.Config{},
wantErr: true,
errContains: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ReadTDXAttestationPolicy(tt.policyPath, tt.policy)
if tt.wantErr {
assert.Error(t, err)
if tt.errContains != "" {
assert.Contains(t, err.Error(), tt.errContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, tt.policy)
}
})
}
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package vtpm
type DummyRWC struct{}
// Read fills p with byte(len(p)) and returns len(p).
func (l *DummyRWC) Read(p []byte) (int, error) {
n := len(p)
// Fill each byte in p with the value of n as a byte.
for i := range p {
p[i] = byte(n)
}
return n, nil
}
// Write simply returns len(p) indicating that all bytes were written.
func (l *DummyRWC) Write(p []byte) (int, error) {
// In this simple implementation, we ignore the data.
return len(p), nil
}
func (l *DummyRWC) Close() error {
return nil
}
+123 -69
View File
@@ -6,9 +6,8 @@ package vtpm
import (
"bytes"
"crypto"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
@@ -16,6 +15,7 @@ import (
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/proto/attest"
@@ -26,26 +26,36 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"golang.org/x/crypto/sha3"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
)
var _ attestation.Provider = (*provider)(nil)
var (
_ attestation.Provider = (*provider)(nil)
_ attestation.Verifier = (*verifier)(nil)
)
const (
eventLog = "/sys/kernel/security/tpm0/binary_bios_measurements"
Nonce = 32
PCR15 = 15
PCR16 = 16
Hash1 = 20
Hash256 = 32
Hash384 = 48
)
var (
ExternalTPM io.ReadWriteCloser
ErrNoHashAlgo = errors.New("hash algo is not supported")
ErrFetchQuote = errors.New("failed to fetch vTPM quote")
ErrFetchAzureToken = errors.New("failed to fetch Azure token")
ExternalTPM io.ReadWriteCloser
ErrNoHashAlgo = errors.New("hash algo is not supported")
ErrFetchQuote = errors.New("failed to fetch vTPM quote")
ErrAttestationPolicyOpen = errors.New("failed to open Attestation Policy file")
ErrAttestationPolicyDecode = errors.New("failed to decode Attestation Policy file")
ErrAttestationPolicyMissing = errors.New("failed due to missing Attestation Policy file")
ErrProtoMarshalFailed = errors.New("failed to marshal protojson")
ErrJsonMarshalFailed = errors.New("failed to marshal json")
ErrJsonUnarshalFailed = errors.New("failed to unmarshal json")
)
type tpm struct {
@@ -93,18 +103,14 @@ func ExtendPCR(pcrIndex int, value []byte) error {
}
type provider struct {
pubKey []byte
teeAttestaion bool
vmpl uint
writer io.Writer
}
func New(pubKey []byte, teeAttestation bool, vmpl uint, writer io.Writer) attestation.Provider {
func NewProvider(teeAttestation bool, vmpl uint) attestation.Provider {
return &provider{
pubKey: pubKey,
teeAttestaion: teeAttestation,
vmpl: vmpl,
writer: writer,
}
}
@@ -125,26 +131,58 @@ func (v provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
return proto.Marshal(quote)
}
func (v provider) VerifTeeAttestation(report []byte, teeNonce []byte) error {
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
}
type verifier struct {
writer io.Writer
Policy *attestation.Config
}
func NewVerifier(writer io.Writer) attestation.Verifier {
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
return &verifier{
writer: writer,
Policy: policy,
}
}
func NewVerifierWithPolicy(pubKey []byte, writer io.Writer, policy *attestation.Config) attestation.Verifier {
if policy == nil {
return NewVerifier(writer)
}
return &verifier{
writer: writer,
Policy: policy,
}
}
func (v verifier) VerifTeeAttestation(report []byte, teeNonce []byte) error {
attestReport, err := abi.ReportToProto(report)
if err != nil {
return errors.Wrap(fmt.Errorf("failed to convert TEE report to proto"), err)
}
attestationReport := sevsnp.Attestation{Report: attestReport, CertificateChain: nil}
return quoteprovider.VerifyAttestationReportTLS(&attestationReport, teeNonce)
return quoteprovider.VerifyAttestationReportTLS(&attestationReport, teeNonce, v.Policy)
}
func (v provider) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return VerifyQuote(report, v.pubKey, vTpmNonce, v.writer)
func (v verifier) VerifVTpmAttestation(report []byte, vTpmNonce []byte) error {
return VerifyQuote(report, vTpmNonce, v.writer, v.Policy)
}
func (v provider) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return VTPMVerify(report, v.pubKey, teeNonce, vTpmNonce, v.writer)
func (v verifier) VerifyAttestation(report []byte, teeNonce []byte, vTpmNonce []byte) error {
return VTPMVerify(report, teeNonce, vTpmNonce, v.writer, v.Policy)
}
func (v provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
return nil, errors.New("Azure attestation token is not supported")
func (v verifier) JSONToPolicy(path string) error {
return ReadPolicy(path, v.Policy)
}
func Attest(teeNonce []byte, vTPMNonce []byte, teeAttestaion bool, vmpl uint) ([]byte, error) {
@@ -163,8 +201,8 @@ func Attest(teeNonce []byte, vTPMNonce []byte, teeAttestaion bool, vmpl uint) ([
return marshalQuote(attestation)
}
func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byte, writer io.Writer) error {
if err := VerifyQuote(quote, pubKeyTLS, vtpmNonce, writer); err != nil {
func VTPMVerify(quote []byte, teeNonce []byte, vtpmNonce []byte, writer io.Writer, policy *attestation.Config) error {
if err := VerifyQuote(quote, vtpmNonce, writer, policy); err != nil {
return fmt.Errorf("failed to verify vTPM quote: %v", err)
}
@@ -175,14 +213,14 @@ func VTPMVerify(quote []byte, pubKeyTLS []byte, teeNonce []byte, vtpmNonce []byt
return errors.Wrap(fmt.Errorf("failed to unmarshal quote"), err)
}
if err := quoteprovider.VerifyAttestationReportTLS(attestation.GetSevSnpAttestation(), teeNonce); err != nil {
if err := quoteprovider.VerifyAttestationReportTLS(attestation.GetSevSnpAttestation(), teeNonce, policy); err != nil {
return fmt.Errorf("failed to verify TEE attestation report: %v", err)
}
return nil
}
func VerifyQuote(quote []byte, pubKeyTLS []byte, vtpmNonce []byte, writer io.Writer) error {
func VerifyQuote(quote []byte, vtpmNonce []byte, writer io.Writer, policy *attestation.Config) error {
attestation := &attest.Attestation{}
err := proto.Unmarshal(quote, attestation)
@@ -206,9 +244,7 @@ func VerifyQuote(quote []byte, pubKeyTLS []byte, vtpmNonce []byte, writer io.Wri
return errors.Wrap(fmt.Errorf("failed to verify attestation"), err)
}
s256, s384 := calculatePCRTLSKey(pubKeyTLS)
if err := checkExpectedPCRValues(attestation, s256, s384); err != nil {
if err := checkExpectedPCRValues(attestation, policy); err != nil {
return fmt.Errorf("PCR values do not match expected PCR values: %w", err)
}
@@ -285,39 +321,23 @@ func addTEEAttestation(attestation *attest.Attestation, nonce []byte, vmpl uint)
return nil
}
func checkExpectedPCRValues(attQuote *attest.Attestation, ePcr256, ePcr384 []byte) error {
func checkExpectedPCRValues(attQuote *attest.Attestation, policy *attestation.Config) error {
quotes := attQuote.GetQuotes()
for i := range quotes {
quote := quotes[i]
var pcrMap map[string]string
var pcr15 []byte
switch quote.Pcrs.Hash {
case ptpm.HashAlgo_SHA256:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha256
if ePcr256 == nil {
pcr15 = make([]byte, 32)
} else {
pcr15 = ePcr256
}
pcrMap = policy.PcrConfig.PCRValues.Sha256
case ptpm.HashAlgo_SHA384:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha384
if ePcr384 == nil {
pcr15 = make([]byte, 48)
} else {
pcr15 = ePcr384
}
pcrMap = policy.PcrConfig.PCRValues.Sha384
case ptpm.HashAlgo_SHA1:
pcrMap = attestation.AttestationPolicy.PcrConfig.PCRValues.Sha1
pcr15 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
pcrMap = policy.PcrConfig.PCRValues.Sha1
default:
return errors.Wrap(ErrNoHashAlgo, fmt.Errorf("algo: %s", ptpm.HashAlgo_name[int32(quote.Pcrs.Hash)]))
}
pcr15Index := uint32(15)
if !bytes.Equal(quote.Pcrs.Pcrs[pcr15Index], pcr15) {
return fmt.Errorf("for algo %s PCR[15] expected %s but found %s", ptpm.HashAlgo_name[int32(quote.Pcrs.Hash)], hex.EncodeToString(pcr15), hex.EncodeToString(quote.Pcrs.Pcrs[pcr15Index]))
}
for i, v := range pcrMap {
index, err := strconv.ParseInt(i, 10, 32)
if err != nil {
@@ -335,27 +355,6 @@ func checkExpectedPCRValues(attQuote *attest.Attestation, ePcr256, ePcr384 []byt
return nil
}
// Return SHA256 and SHA384 values of the input public key.
func calculatePCRTLSKey(pubKey []byte) ([]byte, []byte) {
if len(pubKey) == 0 {
return nil, nil
}
init256 := make([]byte, Hash256)
init384 := make([]byte, Hash384)
key256 := sha3.Sum256(pubKey)
key384 := sha3.Sum384(pubKey)
pcrValue256 := append(init256, key256[:]...)
pcrValue384 := append(init384, key384[:]...)
newPcr256 := sha256.Sum256(pcrValue256)
newPcr384 := sha512.Sum384(pcrValue384)
return newPcr256[:], newPcr384[:]
}
func getPCRValue(index int, algorithm tpm2.Algorithm) ([]byte, error) {
rwc, err := OpenTpm()
if err != nil {
@@ -382,3 +381,58 @@ func GetPCRSHA256Value(index int) ([]byte, error) {
func GetPCRSHA384Value(index int) ([]byte, error) {
return getPCRValue(index, tpm2.AlgSHA384)
}
func ReadPolicy(policyPath string, attestationConfiguration *attestation.Config) error {
if policyPath != "" {
policyData, err := os.ReadFile(policyPath)
if err != nil {
return errors.Wrap(ErrAttestationPolicyOpen, err)
}
return ReadPolicyFromByte(policyData, attestationConfiguration)
}
return ErrAttestationPolicyMissing
}
func ReadPolicyFromByte(policyData []byte, attestationConfiguration *attestation.Config) error {
unmarshalOptions := protojson.UnmarshalOptions{AllowPartial: true, DiscardUnknown: true}
if err := unmarshalOptions.Unmarshal(policyData, attestationConfiguration.Config); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
if err := json.Unmarshal(policyData, attestationConfiguration.PcrConfig); err != nil {
return errors.Wrap(ErrAttestationPolicyDecode, err)
}
return nil
}
func ConvertPolicyToJSON(attestationConfiguration *attestation.Config) ([]byte, error) {
pbJson, err := protojson.Marshal(attestationConfiguration.Config)
if err != nil {
return nil, errors.Wrap(ErrProtoMarshalFailed, err)
}
var pbMap map[string]interface{}
if err := json.Unmarshal(pbJson, &pbMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
pcrJson, err := json.Marshal(attestationConfiguration.PcrConfig)
if err != nil {
return nil, errors.Wrap(ErrJsonMarshalFailed, err)
}
var pcrMap map[string]interface{}
if err := json.Unmarshal(pcrJson, &pcrMap); err != nil {
return nil, errors.Wrap(ErrJsonUnarshalFailed, err)
}
for k, v := range pcrMap {
pbMap[k] = v
}
return json.MarshalIndent(pbMap, "", " ")
}
+866
View File
@@ -0,0 +1,866 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package vtpm
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-tpm-tools/proto/attest"
ptpm "github.com/google/go-tpm-tools/proto/tpm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"google.golang.org/protobuf/encoding/protojson"
)
const sevSnpProductMilan = "Milan"
var policy = attestation.Config{Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}}, PcrConfig: &attestation.PcrConfig{}}
type mockTPM struct {
*bytes.Buffer
closeErr error
}
func (m *mockTPM) Close() error {
return m.closeErr
}
type mockWriter struct {
data []byte
err error
}
func (m *mockWriter) Write(p []byte) (n int, err error) {
if m.err != nil {
return 0, m.err
}
m.data = append(m.data, p...)
return len(p), nil
}
func TestOpenTpm(t *testing.T) {
tests := []struct {
name string
externalTPM io.ReadWriteCloser
expectError bool
}{
{
name: "External TPM available",
externalTPM: &mockTPM{Buffer: &bytes.Buffer{}},
expectError: false,
},
{
name: "No external TPM",
externalTPM: nil,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalExternalTPM := ExternalTPM
defer func() { ExternalTPM = originalExternalTPM }()
ExternalTPM = tt.externalTPM
tpm, err := OpenTpm()
if tt.expectError {
assert.Error(t, err)
} else {
if tt.externalTPM != nil {
assert.NoError(t, err)
assert.NotNil(t, tpm)
}
}
})
}
}
func TestTpmEventLog(t *testing.T) {
tempFile, err := os.CreateTemp("", "event_log")
require.NoError(t, err)
defer os.Remove(tempFile.Name())
testData := []byte("test event log data")
_, err = tempFile.Write(testData)
require.NoError(t, err)
tempFile.Close()
tpm := &tpm{ReadWriteCloser: &mockTPM{Buffer: &bytes.Buffer{}}}
_, err = tpm.EventLog()
assert.Error(t, err)
}
func TestNewProvider(t *testing.T) {
tests := []struct {
name string
teeAttestation bool
vmpl uint
}{
{
name: "TEE attestation enabled",
teeAttestation: true,
vmpl: 1,
},
{
name: "TEE attestation disabled",
teeAttestation: false,
vmpl: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
provider := NewProvider(tt.teeAttestation, tt.vmpl)
assert.NotNil(t, provider)
})
}
}
func TestProviderAzureAttestationToken(t *testing.T) {
provider := NewProvider(false, 0)
token, err := provider.AzureAttestationToken([]byte("test-nonce"))
assert.Error(t, err)
assert.Nil(t, token)
assert.Contains(t, err.Error(), "Azure attestation token is not supported")
}
func TestNewVerifier(t *testing.T) {
writer := &mockWriter{}
verifier := NewVerifier(writer)
assert.NotNil(t, verifier)
}
func TestNewVerifierWithPolicy(t *testing.T) {
writer := &mockWriter{}
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
tests := []struct {
name string
policy *attestation.Config
}{
{
name: "With policy",
policy: policy,
},
{
name: "Without policy (nil)",
policy: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
verifier := NewVerifierWithPolicy([]byte("test-key"), writer, tt.policy)
assert.NotNil(t, verifier)
})
}
}
func TestMarshalQuote(t *testing.T) {
tests := []struct {
name string
attestation *attest.Attestation
expectError bool
}{
{
name: "Valid attestation",
attestation: &attest.Attestation{
AkPub: []byte("test-key"),
},
expectError: false,
},
{
name: "Nil attestation",
attestation: nil,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := marshalQuote(tt.attestation)
if tt.expectError {
assert.Error(t, err)
assert.Empty(t, data)
} else {
assert.NoError(t, err)
if tt.attestation != nil {
assert.NotEmpty(t, data)
}
}
})
}
}
func TestCheckExpectedPCRValues(t *testing.T) {
testPCRValue := make([]byte, 32)
for i := range testPCRValue {
testPCRValue[i] = byte(i)
}
tests := []struct {
name string
attestation *attest.Attestation
policy *attestation.Config
expectError bool
errorMsg string
}{
{
name: "Matching PCR values SHA256",
attestation: &attest.Attestation{
Quotes: []*ptpm.Quote{
{
Pcrs: &ptpm.PCRs{
Hash: ptpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: testPCRValue,
},
},
},
},
},
policy: &attestation.Config{
PcrConfig: &attestation.PcrConfig{
PCRValues: attestation.PcrValues{
Sha256: map[string]string{
"0": hex.EncodeToString(testPCRValue),
},
},
},
},
expectError: false,
},
{
name: "Mismatched PCR values",
attestation: &attest.Attestation{
Quotes: []*ptpm.Quote{
{
Pcrs: &ptpm.PCRs{
Hash: ptpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: testPCRValue,
},
},
},
},
},
policy: &attestation.Config{
PcrConfig: &attestation.PcrConfig{
PCRValues: attestation.PcrValues{
Sha256: map[string]string{
"0": hex.EncodeToString(make([]byte, 32)),
},
},
},
},
expectError: true,
errorMsg: "expected",
},
{
name: "Unsupported hash algorithm",
attestation: &attest.Attestation{
Quotes: []*ptpm.Quote{
{
Pcrs: &ptpm.PCRs{
Hash: ptpm.HashAlgo_HASH_INVALID,
Pcrs: map[uint32][]byte{
0: testPCRValue,
},
},
},
},
},
policy: &attestation.Config{
PcrConfig: &attestation.PcrConfig{},
},
expectError: true,
errorMsg: "hash algo is not supported",
},
{
name: "Invalid PCR index",
attestation: &attest.Attestation{
Quotes: []*ptpm.Quote{
{
Pcrs: &ptpm.PCRs{
Hash: ptpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: testPCRValue,
},
},
},
},
},
policy: &attestation.Config{
PcrConfig: &attestation.PcrConfig{
PCRValues: attestation.PcrValues{
Sha256: map[string]string{
"invalid": hex.EncodeToString(testPCRValue),
},
},
},
},
expectError: true,
errorMsg: "error converting PCR index to int32",
},
{
name: "Invalid PCR value hex",
attestation: &attest.Attestation{
Quotes: []*ptpm.Quote{
{
Pcrs: &ptpm.PCRs{
Hash: ptpm.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: testPCRValue,
},
},
},
},
},
policy: &attestation.Config{
PcrConfig: &attestation.PcrConfig{
PCRValues: attestation.PcrValues{
Sha256: map[string]string{
"0": "invalid-hex",
},
},
},
},
expectError: true,
errorMsg: "error converting PCR value to byte",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkExpectedPCRValues(tt.attestation, tt.policy)
if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestReadPolicy(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy_test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
validPolicy := map[string]interface{}{
"policy": map[string]interface{}{
"product": map[string]interface{}{
"name": "test-product",
},
},
"rootOfTrust": map[string]interface{}{
"productLine": "test-line",
},
"pcrConfig": map[string]interface{}{
"pcrValues": map[string]interface{}{
"sha256": map[string]string{
"0": "0000000000000000000000000000000000000000000000000000000000000000",
},
},
},
}
validPolicyData, err := json.Marshal(validPolicy)
require.NoError(t, err)
validPolicyPath := filepath.Join(tempDir, "valid_policy.json")
err = os.WriteFile(validPolicyPath, validPolicyData, 0o644)
require.NoError(t, err)
tests := []struct {
name string
policyPath string
expectError bool
expectedErr error
}{
{
name: "Valid policy file",
policyPath: validPolicyPath,
expectError: false,
},
{
name: "Non-existent policy file",
policyPath: "/nonexistent/path",
expectError: true,
expectedErr: ErrAttestationPolicyOpen,
},
{
name: "Empty policy path",
policyPath: "",
expectError: true,
expectedErr: ErrAttestationPolicyMissing,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
err := ReadPolicy(tt.policyPath, config)
if tt.expectError {
assert.Error(t, err)
if tt.expectedErr != nil {
assert.True(t, errors.Contains(err, tt.expectedErr))
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestReadPolicyFromByte(t *testing.T) {
tests := []struct {
name string
policyData []byte
expectError bool
expectedErr error
}{
{
name: "Valid policy data",
policyData: []byte(`{
"policy": {
"product": {
"name": "test-product"
}
},
"rootOfTrust": {
"productLine": "test-line"
},
"pcrConfig": {
"pcrValues": {
"sha256": {
"0": "0000000000000000000000000000000000000000000000000000000000000000"
}
}
}
}`),
expectError: false,
},
{
name: "Invalid JSON",
policyData: []byte(`{invalid json`),
expectError: true,
expectedErr: ErrAttestationPolicyDecode,
},
{
name: "Empty policy data",
policyData: []byte(``),
expectError: true,
expectedErr: ErrAttestationPolicyDecode,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
err := ReadPolicyFromByte(tt.policyData, config)
if tt.expectError {
assert.Error(t, err)
if tt.expectedErr != nil {
assert.True(t, errors.Contains(err, tt.expectedErr))
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestConvertPolicyToJSON(t *testing.T) {
tests := []struct {
name string
config *attestation.Config
expectError bool
expectedErr error
}{
{
name: "Valid config",
config: &attestation.Config{
Config: &check.Config{
Policy: &check.Policy{
Product: &sevsnp.SevProduct{
Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN,
},
},
RootOfTrust: &check.RootOfTrust{
ProductLine: "Milan",
},
},
PcrConfig: &attestation.PcrConfig{
PCRValues: attestation.PcrValues{
Sha256: map[string]string{
"0": "0000000000000000000000000000000000000000000000000000000000000000",
},
},
},
},
expectError: false,
},
{
name: "Nil config",
config: &attestation.Config{
Config: nil,
PcrConfig: &attestation.PcrConfig{},
},
expectError: false,
expectedErr: ErrProtoMarshalFailed,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
jsonData, err := ConvertPolicyToJSON(tt.config)
if tt.expectError {
assert.Error(t, err)
if tt.expectedErr != nil {
assert.True(t, errors.Contains(err, tt.expectedErr))
}
assert.Nil(t, jsonData)
} else {
assert.NoError(t, err)
assert.NotNil(t, jsonData)
var result map[string]interface{}
err = json.Unmarshal(jsonData, &result)
assert.NoError(t, err)
}
})
}
}
func TestVTPMVerify(t *testing.T) {
writer := &mockWriter{}
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
tests := []struct {
name string
quote []byte
teeNonce []byte
vtpmNonce []byte
expectError bool
}{
{
name: "Invalid quote data",
quote: []byte("invalid"),
teeNonce: []byte("tee-nonce"),
vtpmNonce: []byte("vtpm-nonce"),
expectError: true,
},
{
name: "Empty quote",
quote: []byte{},
teeNonce: []byte("tee-nonce"),
vtpmNonce: []byte("vtpm-nonce"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VTPMVerify(tt.quote, tt.teeNonce, tt.vtpmNonce, writer, policy)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestVerifyQuote(t *testing.T) {
writer := &mockWriter{}
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
tests := []struct {
name string
quote []byte
vtpmNonce []byte
expectError bool
}{
{
name: "Invalid quote data",
quote: []byte("invalid"),
vtpmNonce: []byte("vtpm-nonce"),
expectError: true,
},
{
name: "Empty quote",
quote: []byte{},
vtpmNonce: []byte("vtpm-nonce"),
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := VerifyQuote(tt.quote, tt.vtpmNonce, writer, policy)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestWriterError(t *testing.T) {
writer := &mockWriter{err: fmt.Errorf("write error")}
policy := &attestation.Config{
Config: &check.Config{Policy: &check.Policy{}, RootOfTrust: &check.RootOfTrust{}},
PcrConfig: &attestation.PcrConfig{},
}
err := VerifyQuote([]byte("invalid"), []byte("nonce"), writer, policy)
assert.Error(t, err)
}
func TestVerifyAttestationReportMalformedSignature(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
// Change random data so in the signature so the signature fails
attestationPB.Report.Signature[0] = attestationPB.Report.Signature[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, distorted signature",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportUnknownProduct(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
err = changeProductAttestationPolicy()
require.NoError(t, err)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, unknown product",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrProductLine,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportSuccess(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
goodProduct int
err error
}{
{
name: "Valid attestation, validation and verification is performed succsessfully",
attestationReport: attestationPB,
reportData: reportData,
goodProduct: 1,
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func TestVerifyAttestationReportMalformedPolicy(t *testing.T) {
tempDir, err := os.MkdirTemp("", "policy")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
attestationPB, reportData := prepVerifyAttReport(t)
err = setAttestationPolicy(attestationPB, tempDir)
require.NoError(t, err)
// Change random data in the measurement so the measurement does not match
attestationPB.Report.Measurement[0] = attestationPB.Report.Measurement[0] ^ 0x01
tests := []struct {
name string
attestationReport *sevsnp.Attestation
reportData []byte
err error
}{
{
name: "Valid attestation, malformed policy (measurement)",
attestationReport: attestationPB,
reportData: reportData,
err: quoteprovider.ErrAttVerification,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := quoteprovider.VerifyAttestationReportTLS(tt.attestationReport, tt.reportData, &policy)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
}
func prepVerifyAttReport(t *testing.T) (*sevsnp.Attestation, []byte) {
file, err := os.ReadFile("../../../attestation.bin")
require.NoError(t, err)
if len(file) < abi.ReportSize {
file = append(file, make([]byte, abi.ReportSize-len(file))...)
}
rr, err := abi.ReportCertsToProto(file)
require.NoError(t, err)
return rr, rr.Report.ReportData
}
func setAttestationPolicy(rr *sevsnp.Attestation, policyDirectory string) error {
attestationPolicyFile, err := os.ReadFile("../../../scripts/attestation_policy/attestation_policy.json")
if err != nil {
return err
}
unmarshalOptions := protojson.UnmarshalOptions{DiscardUnknown: true}
err = unmarshalOptions.Unmarshal(attestationPolicyFile, policy)
if err != nil {
return err
}
policy.Config.Policy.Product = &sevsnp.SevProduct{Name: sevsnp.SevProduct_SEV_PRODUCT_MILAN}
policy.Config.Policy.FamilyId = rr.Report.FamilyId
policy.Config.Policy.ImageId = rr.Report.ImageId
policy.Config.Policy.Measurement = rr.Report.Measurement
policy.Config.Policy.HostData = rr.Report.HostData
policy.Config.Policy.ReportIdMa = rr.Report.ReportIdMa
policy.Config.RootOfTrust.ProductLine = sevSnpProductMilan
policyByte, err := ConvertPolicyToJSON(&policy)
if err != nil {
return err
}
policyPath := filepath.Join(policyDirectory, "attestation_policy.json")
err = os.WriteFile(policyPath, policyByte, 0o644)
if err != nil {
return nil
}
attestation.AttestationPolicyPath = policyPath
return nil
}
func changeProductAttestationPolicy() error {
err := ReadPolicy(attestation.AttestationPolicyPath, &policy)
if err != nil {
return err
}
policy.Config.RootOfTrust.ProductLine = ""
policy.Config.Policy.Product = nil
policyByte, err := ConvertPolicyToJSON(&policy)
if err != nil {
return err
}
if err := os.WriteFile(attestation.AttestationPolicyPath, policyByte, 0o644); err != nil {
return nil
}
return nil
}
+1 -2
View File
@@ -15,7 +15,6 @@ import (
"github.com/ultravioletrs/cocos/agent"
agentgrpc "github.com/ultravioletrs/cocos/agent/api/grpc"
"github.com/ultravioletrs/cocos/agent/mocks"
"github.com/ultravioletrs/cocos/pkg/attestation"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
@@ -113,7 +112,7 @@ func TestAgentClientIntegration(t *testing.T) {
},
AttestedTLS: true,
},
err: attestation.ErrAttestationPolicyMissing,
err: errors.New("failed to stat attestation policy file"),
},
}
+43 -42
View File
@@ -1,19 +1,16 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
//go:build cgo
package grpc
import (
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"net"
"os"
"strconv"
"time"
"github.com/absmach/magistrala/pkg/errors"
@@ -24,17 +21,21 @@ import (
func setupATLS(cfg AgentClientConfig) (credentials.TransportCredentials, security, error) {
security := withaTLS
err := attestation.ReadAttestationPolicy(cfg.AttestationPolicy, &attestation.AttestationPolicy)
info, err := os.Stat(cfg.AttestationPolicy)
if err != nil {
return nil, withoutTLS, errors.Wrap(fmt.Errorf("failed to read Attestation Policy"), err)
return nil, withoutTLS, errors.Wrap(fmt.Errorf("failed to stat attestation policy file"), err)
}
var insecureSkipVerify bool = true
if !info.Mode().IsRegular() {
return nil, withoutTLS, fmt.Errorf("attestation policy file is not a regular file: %s", cfg.AttestationPolicy)
}
attestation.AttestationPolicyPath = cfg.AttestationPolicy
var rootCAs *x509.CertPool = nil
if len(cfg.ServerCAFile) > 0 {
insecureSkipVerify = false
// Read the certificate file
certPEM, err := os.ReadFile(cfg.ServerCAFile)
if err != nil {
@@ -59,11 +60,20 @@ func setupATLS(cfg AgentClientConfig) (credentials.TransportCredentials, securit
security = withmaTLS
}
nonce := make([]byte, 64)
if _, err := rand.Read(nonce); err != nil {
return nil, withoutTLS, errors.Wrap(fmt.Errorf("failed to generate nonce"), err)
}
encoded := hex.EncodeToString(nonce)
sni := fmt.Sprintf("%s.nonce", encoded)
tlsConfig := &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
InsecureSkipVerify: true,
RootCAs: rootCAs,
ServerName: sni,
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
return verifyPeerCertificateATLS(rawCerts, verifiedChains, cfg)
return verifyPeerCertificateATLS(rawCerts, verifiedChains, nonce, rootCAs)
},
}
@@ -78,49 +88,40 @@ func setupATLS(cfg AgentClientConfig) (credentials.TransportCredentials, securit
return credentials.NewTLS(tlsConfig), security, nil
}
func CustomDialer(ctx context.Context, addr string) (net.Conn, error) {
ip, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("could not create a custom dialer")
}
p, err := strconv.Atoi(port)
if err != nil {
return nil, fmt.Errorf("bad format of IP address: %v", err)
}
conn, err := atls.DialTLSClient(ip, p)
if err != nil {
return nil, fmt.Errorf("could not create TLS connection")
}
return conn, nil
}
func verifyPeerCertificateATLS(rawCerts [][]byte, verifiedChains [][]*x509.Certificate, cfg AgentClientConfig) error {
if len(cfg.ServerCAFile) > 0 {
return nil
}
func verifyPeerCertificateATLS(rawCerts [][]byte, verifiedChains [][]*x509.Certificate, nonce []byte, rootCAs *x509.CertPool) error {
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return errors.Wrap(errCertificateParse, err)
}
err = checkIfCertificateSelfSigned(cert)
err = checkIfCertificateSigned(cert, rootCAs)
if err != nil {
return errors.Wrap(errAttVerification, err)
}
return nil
for _, ext := range cert.Extensions {
pType, err := atls.GetPlatformTypeFromOID(ext.Id)
if err == nil {
pubKeyDER, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
if err != nil {
return fmt.Errorf("failed to marshal public key to DER format: %w", err)
}
return atls.VerifyCertificateExtension(ext.Value, pubKeyDER, nonce, pType)
}
}
return fmt.Errorf("attestation extension not found in certificate")
}
func checkIfCertificateSelfSigned(cert *x509.Certificate) error {
certPool := x509.NewCertPool()
certPool.AddCert(cert)
func checkIfCertificateSigned(cert *x509.Certificate, rootCAs *x509.CertPool) error {
if rootCAs == nil {
rootCAs = x509.NewCertPool()
rootCAs.AddCert(cert)
}
opts := x509.VerifyOptions{
Roots: certPool,
Roots: rootCAs,
CurrentTime: time.Now(),
}
-16
View File
@@ -1,16 +0,0 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
//go:build !cgo
package grpc
import (
"fmt"
"google.golang.org/grpc/credentials"
)
func setupATLS(cfg AgentClientConfig) (credentials.TransportCredentials, error) {
return nil, fmt.Errorf("aTLS is not supported without CGO. Please rebuild with CGO_ENABLED=1")
}
+8 -7
View File
@@ -20,6 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
)
func TestNewClient(t *testing.T) {
@@ -108,7 +109,7 @@ func TestNewClient(t *testing.T) {
AttestationPolicy: "no such file",
},
wantErr: true,
err: fmt.Errorf("failed to read Attestation Policy"),
err: fmt.Errorf("failed to stat attestation policy file"),
},
{
name: "Fail with invalid ServerCAFile",
@@ -226,25 +227,25 @@ func TestReadAttestationPolicy(t *testing.T) {
name: "Invalid JSON",
manifestPath: "invalid_manifest.json",
fileContent: invalidJSON,
err: attestation.ErrAttestationPolicyDecode,
err: vtpm.ErrAttestationPolicyDecode,
},
{
name: "Non-existent file",
manifestPath: "nonexistent.json",
fileContent: "",
err: attestation.ErrAttestationPolicyOpen,
err: vtpm.ErrAttestationPolicyOpen,
},
{
name: "Empty manifest path",
manifestPath: "",
fileContent: "",
err: attestation.ErrAttestationPolicyMissing,
err: vtpm.ErrAttestationPolicyMissing,
},
{
name: "Invalid JSON PCR",
manifestPath: "invalid_manifest.json",
fileContent: invalidJSONPCR,
err: attestation.ErrAttestationPolicyDecode,
err: vtpm.ErrAttestationPolicyDecode,
},
}
@@ -257,7 +258,7 @@ func TestReadAttestationPolicy(t *testing.T) {
}
config := attestation.Config{Config: &check.Config{}, PcrConfig: &attestation.PcrConfig{}}
err := attestation.ReadAttestationPolicy(tt.manifestPath, &config)
err := vtpm.ReadPolicy(tt.manifestPath, &config)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
if tt.err == nil {
@@ -376,7 +377,7 @@ func TestCheckIfCertificateSelfSigned(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkIfCertificateSelfSigned(tt.cert)
err := checkIfCertificateSigned(tt.cert, nil)
assert.True(t, errors.Contains(err, tt.err), fmt.Sprintf("expected error %v, got %v", tt.err, err))
})
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cvm
import (
"fmt"
"net"
"testing"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent"
agentgrpc "github.com/ultravioletrs/cocos/agent/api/grpc"
"github.com/ultravioletrs/cocos/agent/mocks"
pkggrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
grpchealth "google.golang.org/grpc/health/grpc_health_v1"
)
type TestServer struct {
agent.UnimplementedAgentServiceServer
server *grpc.Server
health *health.Server
port int
listenAddr string
}
func NewTestServer() (*TestServer, error) {
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
return nil, fmt.Errorf("failed to listen: %v", err)
}
addr := listener.Addr().(*net.TCPAddr)
server := grpc.NewServer()
healthServer := health.NewServer()
ts := &TestServer{
server: server,
health: healthServer,
port: addr.Port,
listenAddr: fmt.Sprintf("localhost:%d", addr.Port),
}
svc := new(mocks.Service)
agent.RegisterAgentServiceServer(server, agentgrpc.NewServer(svc))
grpchealth.RegisterHealthServer(server, healthServer)
go func() {
if err := server.Serve(listener); err != nil {
fmt.Printf("Server exited with error: %v\n", err)
}
}()
healthServer.SetServingStatus("agent", grpchealth.HealthCheckResponse_SERVING)
return ts, nil
}
func (s *TestServer) Stop() {
if s.server != nil {
s.server.GracefulStop()
}
}
func TestAgentClientIntegration(t *testing.T) {
testServer, err := NewTestServer()
require.NoError(t, err)
defer testServer.Stop()
time.Sleep(100 * time.Millisecond)
tests := []struct {
name string
serverRunning bool
config pkggrpc.CVMClientConfig
err error
}{
{
name: "successful connection",
serverRunning: true,
config: pkggrpc.CVMClientConfig{
BaseConfig: pkggrpc.BaseConfig{
URL: testServer.listenAddr,
Timeout: 1,
},
},
err: nil,
},
{
name: "server not healthy",
serverRunning: false,
config: pkggrpc.CVMClientConfig{
BaseConfig: pkggrpc.BaseConfig{
URL: "",
Timeout: 1,
},
},
err: errors.New("failed to connect to grpc server"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.serverRunning {
testServer.health.SetServingStatus("agent", grpchealth.HealthCheckResponse_NOT_SERVING)
} else {
testServer.health.SetServingStatus("agent", grpchealth.HealthCheckResponse_SERVING)
}
client, agentClient, err := NewCVMClient(tt.config)
assert.True(t, errors.Contains(err, tt.err))
if err != nil {
assert.Nil(t, client)
assert.Nil(t, agentClient)
return
}
require.NotNil(t, client)
require.NotNil(t, agentClient)
defer client.Close()
})
}
}
-1
View File
@@ -148,7 +148,6 @@ func connect(cfg ClientConfiguration) (*grpc.ClientConn, security, error) {
}
opts = append(opts, grpc.WithTransportCredentials(tc))
opts = append(opts, grpc.WithContextDialer(CustomDialer))
secure = sec
} else {
+70
View File
@@ -529,6 +529,76 @@ func TestReceiveAttestation(t *testing.T) {
}
}
func TestReceiverIMAMeasurements(t *testing.T) {
tests := []struct {
name string
description string
totalSize int
chunks [][]byte
wantResult []byte
wantErr error
}{
{
name: "successful single chunk receive",
description: "Receiving IMA measurements",
totalSize: 20,
chunks: [][]byte{[]byte("12345678912345678999")},
wantResult: []byte("12345678912345678999"),
wantErr: nil,
},
{
name: "stream error",
description: "Receiving IMA measurements",
totalSize: 20,
chunks: [][]byte{[]byte("12345678912345678999")},
wantResult: nil,
wantErr: errors.New("stream error"),
},
{
name: "size mismatch",
description: "Receiving IMA measurements",
totalSize: 10,
chunks: [][]byte{[]byte("12345678912345678999")},
wantResult: nil,
wantErr: errors.New("progress update exceeds total bytes: attempted to add 20 bytes, but only 10 bytes remain"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockStream := new(mocks.AgentService_IMAMeasurementsClient[agent.IMAMeasurementsResponse])
p := New(true)
p.TerminalWidthFunc = func() (int, error) { return 100, nil }
resultFile, err := os.CreateTemp("", "test_ima_measurements")
assert.NoError(t, err)
t.Cleanup(func() {
os.Remove(resultFile.Name())
})
if tt.wantErr != nil {
mockStream.On("Recv").Return(nil, tt.wantErr).Once()
}
mockStream.On("Recv").Return(&agent.IMAMeasurementsResponse{Pcr10: []byte(tt.chunks[0]), File: []byte(tt.chunks[0])}, nil).Once()
mockStream.On("Recv").Return(nil, io.EOF).Once()
pcr10, err := p.ReceiveIMAMeasurements(tt.description, tt.totalSize, mockStream, resultFile)
assert.NoError(t, resultFile.Close())
if tt.wantErr != nil {
assert.Error(t, err)
assert.Equal(t, tt.wantErr.Error(), err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, tt.wantResult, pcr10)
}
})
}
}
type mockAlgoStream struct {
stream agent.AgentService_AlgoClient
sendCount int
+29 -29
View File
@@ -176,6 +176,35 @@ func (sdk *agentSDK) AttestationResult(ctx context.Context, nonce [size32]byte,
return nil
}
func (sdk *agentSDK) IMAMeasurements(ctx context.Context, resultFile *os.File) ([]byte, error) {
request := &agent.IMAMeasurementsRequest{}
stream, err := sdk.client.IMAMeasurements(ctx, request)
if err != nil {
return nil, err
}
incomingmd, err := stream.Header()
if err != nil {
return nil, err
}
fileSizeStr := incomingmd.Get(grpc.FileSizeKey)
if len(fileSizeStr) == 0 {
fileSizeStr = append(fileSizeStr, "0")
}
fileSize, err := strconv.Atoi(fileSizeStr[0])
if err != nil {
return nil, err
}
pb := progressbar.New(true)
return pb.ReceiveIMAMeasurements(imaMeasurementsProgressDescription, fileSize, stream, resultFile)
}
func signData(userID string, privKey crypto.Signer) ([]byte, error) {
var signature []byte
var err error
@@ -208,32 +237,3 @@ func generateMetadata(userID string, privateKey crypto.PrivateKey) (metadata.MD,
kv[auth.SignatureMetadataKey] = base64.StdEncoding.EncodeToString(signature)
return metadata.New(kv), nil
}
func (sdk *agentSDK) IMAMeasurements(ctx context.Context, resultFile *os.File) ([]byte, error) {
request := &agent.IMAMeasurementsRequest{}
stream, err := sdk.client.IMAMeasurements(ctx, request)
if err != nil {
return nil, err
}
incomingmd, err := stream.Header()
if err != nil {
return nil, err
}
fileSizeStr := incomingmd.Get(grpc.FileSizeKey)
if len(fileSizeStr) == 0 {
fileSizeStr = append(fileSizeStr, "0")
}
fileSize, err := strconv.Atoi(fileSizeStr[0])
if err != nil {
return nil, err
}
pb := progressbar.New(true)
return pb.ReceiveIMAMeasurements(imaMeasurementsProgressDescription, fileSize, stream, resultFile)
}
+76
View File
@@ -558,6 +558,82 @@ func TestAttestationResult(t *testing.T) {
}
}
func TestIMAMeasurements(t *testing.T) {
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
response := &agent.IMAMeasurementsResponse{
File: []byte{
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
},
}
cases := []struct {
name string
response *agent.IMAMeasurementsResponse
svcRes []byte
err error
}{
{
name: "fetch IMA measurements successfully",
response: response,
svcRes: response.File,
err: nil,
},
{
name: "failed to fetch IMA measurements",
response: &agent.IMAMeasurementsResponse{File: []byte{}},
svcRes: nil,
err: errors.New("failed to fetch IMA measurements"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("IMAMeasurements", mock.Anything).Return(tc.svcRes, tc.svcRes, tc.err)
file, err := os.CreateTemp("", "ima_measurements")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(file.Name())
})
_, err = sdk.IMAMeasurements(context.Background(), file)
require.NoError(t, file.Close())
st, ok := status.FromError(err)
if !ok {
t.Fatalf("Expected gRPC status error, but got: %v", err)
}
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
res, err := os.ReadFile(file.Name())
require.NoError(t, err)
assert.Equal(t, tc.response.File, res, tc.name)
svcCall.Unset()
})
}
}
func generateKeys(t *testing.T, keyType string) (priv any, pub []byte) {
switch keyType {
case "ecdsa":
+13 -13
View File
@@ -14,9 +14,9 @@ const SEV_FAMILY: u32 = 0xF;
const MILAN_EXTENDED_MODEL: u32 = 0x0;
const GENOA_EXTENDED_MODEL: u32 = 0x1;
const SEV_PRODUCT_UNKNOWN: i32 = 0;
const SEV_PRODUCT_MILAN: i32 = 1;
const SEV_PRODUCT_GENOA: i32 = 2;
const SEV_SNP_PRODUCT_UNKNOWN: i32 = 0;
const SEV_SNP_PRODUCT_MILAN: i32 = 1;
const SEV_SNP_PRODUCT_GENOA: i32 = 2;
#[derive(Clone, Copy, Serialize)]
struct SevProduct {
@@ -64,8 +64,8 @@ fn get_sev_snp_processor() -> u32 {
fn get_product_name(product: i32) -> String {
match product {
SEV_PRODUCT_MILAN => "Milan".to_string(),
SEV_PRODUCT_GENOA => "Genoa".to_string(),
SEV_SNP_PRODUCT_MILAN => "Milan".to_string(),
SEV_SNP_PRODUCT_GENOA => "Genoa".to_string(),
_ => "Unknown".to_string(),
}
}
@@ -84,15 +84,15 @@ fn sev_product(eax: u32) -> SevProduct {
let extended_model = (eax >> EXTENDED_MODEL_SHIFT) & 0xf;
let family = (eax >> FAMILY_SHIFT) & 0xf;
let mut product_name = SEV_PRODUCT_UNKNOWN;
let mut product_name = SEV_SNP_PRODUCT_UNKNOWN;
if extended_family == SEV_EXTENDED_FAMILY && family == SEV_FAMILY {
product_name = match extended_model {
MILAN_EXTENDED_MODEL => SEV_PRODUCT_MILAN,
GENOA_EXTENDED_MODEL => SEV_PRODUCT_GENOA,
MILAN_EXTENDED_MODEL => SEV_SNP_PRODUCT_MILAN,
GENOA_EXTENDED_MODEL => SEV_SNP_PRODUCT_GENOA,
_ => {
return SevProduct {
name: SEV_PRODUCT_UNKNOWN,
name: SEV_SNP_PRODUCT_UNKNOWN,
};
}
};
@@ -184,15 +184,15 @@ fn main() {
// If the PCR file path was provided, read and merge its JSON content.
if let Some(pcr_path) = pcr_path {
let pcr_content = read_to_string(pcr_path)
.unwrap_or_else(|_| panic!("Failed to read PCR file at {}", pcr_path));
.unwrap_or_else(|_| panic!("Failed to read PCR file at {pcr_path}"));
let pcr_value: Value = serde_json::from_str(&pcr_content)
.unwrap_or_else(|_| panic!("Failed to parse PCR JSON file at {}", pcr_path));
.unwrap_or_else(|_| panic!("Failed to parse PCR JSON file at {pcr_path}"));
if let Value::Object(ref mut main_map) = computation_value {
if let Value::Object(pcr_map) = pcr_value {
main_map.extend(pcr_map);
} else {
eprintln!("PCR file {} is not a JSON object.", pcr_path);
eprintln!("PCR file {pcr_path} is not a JSON object.");
}
} else {
eprintln!("The computed JSON is not an object.");
@@ -203,5 +203,5 @@ fn main() {
let merged_json =
serde_json::to_string_pretty(&computation_value).expect("Failed to serialize merged JSON");
println!("{}", merged_json);
println!("{merged_json}");
}
+1 -4
View File
@@ -141,10 +141,7 @@ func main() {
attestedTLS = attestedTLSBoolValue
}
if dataPathString == "" {
parsingErrorString.WriteString("Date source paths are required\n")
parsingError = true
} else {
if dataPathString != "" {
dataPaths = strings.Split(dataPathString, ",")
}