mirror of
https://github.com/ultravioletrs/cocos.git
synced 2026-06-23 04:10:25 +00:00
COCOS-577 - Introduce Go-based CoRIM generation and deprecate Rust attestation policy scripts. (#578)
CI / lint (push) Has been cancelled
CI / test (agent) (push) Has been cancelled
CI / test (cli) (push) Has been cancelled
CI / test (cmd) (push) Has been cancelled
CI / test (internal) (push) Has been cancelled
CI / test (manager, true) (push) Has been cancelled
CI / test (pkg) (push) Has been cancelled
CI / upload-coverage (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (agent) (push) Has been cancelled
CI / test (cli) (push) Has been cancelled
CI / test (cmd) (push) Has been cancelled
CI / test (internal) (push) Has been cancelled
CI / test (manager, true) (push) Has been cancelled
CI / test (pkg) (push) Has been cancelled
CI / upload-coverage (push) Has been cancelled
* feat: Introduce Go-based CoRIM generation and deprecate Rust attestation policy scripts. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Update dependencies and refactor attestation policy handling Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Migrate attestation verification to use CoRIM and remove deprecated policy handling and EAT verification tests. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * Removed the `tdx` and `sev-snp` attestation policy scripts and their build configurations, along with related build and installation steps from the main Makefile. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * chore: Remove Rust CI workflow and Cargo Dependabot configuration, and enhance Go test setup for attestation policy paths. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * refactor: Use WriteString instead of Write([]byte) for writing policy file content in test. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Refactor `ca-bundle` command to fetch bundles by product string using a configurable HTTP getter with improved error handling, and simplify `attestation_policy` command usage. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * fix: ignore return value of cmd.Help() Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Implement CoRIM generation for Azure and GCP attestation policies and add a CLI command to download and verify GCP OVMF files. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Upgrade Python virtual environment setup to include setuptools and wheel, append computation ID to Docker container names, and improve test robustness with error assertions and conditional skips for runtime tests. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * test: Enhance attestation verification tests, including CoRIM integration and specific platform types like Azure SNP, vTPM, TDX, and IGVM. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Add comprehensive test cases for `VerifyWithCoRIM` including success and measurement mismatch, and refine reference value validation. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Add Azure and TDX attestation verification tests and abstract external service dependencies for improved testability. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * feat: Add new test cases for Azure measurement extraction, EAT platform types, IGVM measurement stopping, vTPM CoRIM verification, and GCP OVMF download CLI. Signed-off-by: Sammy Oina <sammyoina@gmail.com> * test: enhance CLI CoRIM generation and ATLS certificate verification tests, and refactor the Azure MAA client to use an interface. Signed-off-by: Sammy Oina <sammyoina@gmail.com> --------- Signed-off-by: Sammy Oina <sammyoina@gmail.com>
This commit is contained in:
committed by
GitHub
parent
da31d76c94
commit
c1cbcec851
@@ -0,0 +1,196 @@
|
||||
# CoRIM Generation CLI Commands
|
||||
|
||||
This document describes the CLI commands for generating CoRIM (Concise Reference Integrity Manifest) attestation policies.
|
||||
|
||||
## Overview
|
||||
|
||||
The `cocos-cli policy create-corim` command provides subcommands for generating CoRIM policies for different platforms:
|
||||
- **azure**: Generate from Azure Attestation Token
|
||||
- **gcp**: Generate from GCP endorsements
|
||||
- **snp**: Generate for AMD SEV-SNP (direct host generation)
|
||||
- **tdx**: Generate for Intel TDX (direct host generation)
|
||||
|
||||
## Commands
|
||||
|
||||
### Azure SEV-SNP
|
||||
|
||||
Generate CoRIM from an Azure Attestation Token (JWT).
|
||||
|
||||
```bash
|
||||
cocos-cli policy create-corim azure --token <path-to-token> [--product <product>]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--token` (required): Path to file containing Azure Attestation Token (JWT)
|
||||
- `--product` (optional): Processor product name (default: "Milan")
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
cocos-cli policy create-corim azure \
|
||||
--token /path/to/token.jwt \
|
||||
--product Milan \
|
||||
> azure-policy.corim
|
||||
```
|
||||
|
||||
### GCP SEV-SNP
|
||||
|
||||
Generate CoRIM from GCP SEV-SNP measurement and endorsements.
|
||||
|
||||
```bash
|
||||
cocos-cli policy create-corim gcp --measurement <hex> [--vcpu <num>]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--measurement` (required): 384-bit measurement hex string
|
||||
- `--vcpu` (optional): vCPU number (default: 0)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
cocos-cli policy create-corim gcp \
|
||||
--measurement abc123... \
|
||||
--vcpu 0 \
|
||||
> gcp-policy.corim
|
||||
```
|
||||
|
||||
### SEV-SNP (Direct Host)
|
||||
|
||||
Generate CoRIM for AMD SEV-SNP platform directly on the host.
|
||||
|
||||
```bash
|
||||
cocos-cli policy create-corim snp [flags]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--measurement` (optional): Measurement/Launch Digest (hex string, defaults to zero if not provided)
|
||||
- `--policy` (optional): SNP policy flags (default: 0)
|
||||
- `--svn` (optional): Security Version Number/TCB (default: 0)
|
||||
- `--product` (optional): Processor product name (default: "Milan")
|
||||
- `--host-data` (optional): Host data (hex string)
|
||||
- `--launch-tcb` (optional): Minimum launch TCB (default: 0)
|
||||
- `--output` (optional): Output file path (default: stdout)
|
||||
|
||||
**Examples:**
|
||||
|
||||
Generate with defaults (zeroed measurement):
|
||||
```bash
|
||||
cocos-cli policy create-corim snp \
|
||||
--product Milan \
|
||||
--output snp-policy.corim
|
||||
```
|
||||
|
||||
Generate with custom measurement:
|
||||
```bash
|
||||
cocos-cli policy create-corim snp \
|
||||
--measurement abc123def456... \
|
||||
--product Genoa \
|
||||
--svn 1 \
|
||||
--policy 0x30000 \
|
||||
--output snp-policy.corim
|
||||
```
|
||||
|
||||
Generate with host data and launch TCB:
|
||||
```bash
|
||||
cocos-cli policy create-corim snp \
|
||||
--measurement abc123... \
|
||||
--host-data deadbeef \
|
||||
--launch-tcb 1 \
|
||||
--output snp-policy.corim
|
||||
```
|
||||
|
||||
### TDX (Direct Host)
|
||||
|
||||
Generate CoRIM for Intel TDX platform directly on the host.
|
||||
|
||||
```bash
|
||||
cocos-cli policy create-corim tdx [flags]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--measurement` (optional): MRTD measurement (hex string, uses default if not provided)
|
||||
- `--svn` (optional): Security Version Number (default: 0)
|
||||
- `--rtmrs` (optional): Comma-separated RTMRs (hex)
|
||||
- `--mr-seam` (optional): MRSEAM (hex)
|
||||
- `--output` (optional): Output file path (default: stdout)
|
||||
|
||||
**Examples:**
|
||||
|
||||
Generate with defaults (matches legacy script behavior):
|
||||
```bash
|
||||
cocos-cli policy create-corim tdx \
|
||||
--output tdx-policy.corim
|
||||
```
|
||||
|
||||
Generate with custom values:
|
||||
```bash
|
||||
cocos-cli policy create-corim tdx \
|
||||
--measurement abc123def456... \
|
||||
--rtmrs rtmr0,rtmr1,rtmr2,rtmr3 \
|
||||
--mr-seam 789abc... \
|
||||
--svn 2 \
|
||||
--output tdx-policy.corim
|
||||
```
|
||||
|
||||
## Signing CoRIMs
|
||||
|
||||
CoRIMs can be signed using a private key (COSE_Sign1). The generated output will be a COSE-wrapped CoRIM in CBOR format.
|
||||
|
||||
### Prerequisite: Generate Signing Key
|
||||
|
||||
You will need an EC private key (P-256) in PEM format. You can generate one using `openssl`:
|
||||
|
||||
```bash
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
|
||||
```
|
||||
|
||||
### Signing with CLI
|
||||
|
||||
Use the `--signing-key` flag to sign the CoRIM during generation.
|
||||
|
||||
**SNP Example:**
|
||||
```bash
|
||||
cocos-cli policy create-corim snp \
|
||||
--product Milan \
|
||||
--signing-key private-key.pem \
|
||||
--output signed-snp.corim
|
||||
```
|
||||
|
||||
**TDX Example:**
|
||||
```bash
|
||||
cocos-cli policy create-corim tdx \
|
||||
--signing-key private-key.pem \
|
||||
--output signed-tdx.corim
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
The output file is a standard COSE_Sign1 message containing the CoRIM. It can be verified using any tool that supports COSE and CoRIM verification, such as the [veraison/corim](https://github.com/veraison/corim) library.
|
||||
|
||||
## Output Format
|
||||
|
||||
All commands output CoRIM in CBOR (Concise Binary Object Representation) format. By default, output is written to stdout, allowing for piping:
|
||||
|
||||
```bash
|
||||
# Pipe to file
|
||||
cocos-cli policy create-corim snp --product Milan > policy.corim
|
||||
|
||||
# Pipe to another command
|
||||
cocos-cli policy create-corim tdx | base64
|
||||
|
||||
# Use --output flag
|
||||
cocos-cli policy create-corim snp --product Milan --output policy.corim
|
||||
```
|
||||
|
||||
## Integration with Manager
|
||||
|
||||
The manager service can dynamically generate CoRIM policies using the same underlying generator package. When `FetchAttestationPolicy` is called:
|
||||
|
||||
1. For SNP: Calculates IGVM measurement using the `igvmmeasure` binary
|
||||
2. Extracts host data and launch TCB from VM configuration
|
||||
3. Generates CoRIM using the `generator` package
|
||||
4. Returns CBOR-encoded CoRIM
|
||||
|
||||
## See Also
|
||||
|
||||
- [Generator Package Documentation](../pkg/attestation/generator/README.md)
|
||||
- [IGVM Measure Package Documentation](../pkg/attestation/igvmmeasure/README.md)
|
||||
- [Manager README](../manager/README.md)
|
||||
+5
-227
@@ -6,22 +6,16 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/fatih/color"
|
||||
"github.com/google/go-sev-guest/abi"
|
||||
"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"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"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"
|
||||
"google.golang.org/protobuf/encoding/prototext"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -36,6 +30,7 @@ const (
|
||||
attestationFilePath = "attestation.bin"
|
||||
azureAttestResultFilePath = "azure_attest_result.json"
|
||||
azureAttestTokenFilePath = "azure_attest_token.jwt"
|
||||
attestationReportJson = "attestation.json"
|
||||
TEE = "tee"
|
||||
SNP = "snp"
|
||||
VTPM = "vtpm"
|
||||
@@ -48,38 +43,14 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
mode string
|
||||
cfgString string
|
||||
timeout time.Duration
|
||||
maxRetryDelay time.Duration
|
||||
platformInfo string
|
||||
stepping string
|
||||
trustedAuthorKeys []string
|
||||
trustedAuthorHashes []string
|
||||
trustedIdKeys []string
|
||||
trustedIdKeyHashes []string
|
||||
attestationFile string
|
||||
attestationRaw []byte
|
||||
empty16 = [size16]byte{}
|
||||
empty32 = [size32]byte{}
|
||||
empty64 = [size64]byte{}
|
||||
defaultReportIdMa = []byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
|
||||
errReportSize = errors.New("attestation contents too small")
|
||||
ErrBadAttestation = errors.New("attestation file is corrupted or in wrong format")
|
||||
output string
|
||||
nonce []byte
|
||||
format string
|
||||
teeNonce []byte
|
||||
tokenNonce []byte
|
||||
getTextProtoAttestationReport bool
|
||||
getAzureTokenJWT bool
|
||||
cloud string
|
||||
reportData []byte
|
||||
checkCrl bool
|
||||
)
|
||||
|
||||
var errEmptyFile = errors.New("input file is empty")
|
||||
|
||||
func (cli *CLI) NewAttestationCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "attestation [command]",
|
||||
@@ -302,186 +273,14 @@ func attestationToJSON(report []byte) ([]byte, error) {
|
||||
return json.MarshalIndent(attestationPB, "", " ")
|
||||
}
|
||||
|
||||
func attestationFromJSON(reportFile []byte) ([]byte, error) {
|
||||
var attestationPB sevsnp.Attestation
|
||||
if err := json.Unmarshal(reportFile, &attestationPB); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return report.Transform(&attestationPB, "bin")
|
||||
}
|
||||
|
||||
func isFileJSON(filename string) bool {
|
||||
return strings.HasSuffix(filename, ".json")
|
||||
}
|
||||
|
||||
func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
return &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 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>`,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
mode, _ := cmd.Flags().GetString("mode")
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("please pass the attestation report file path")
|
||||
}
|
||||
|
||||
// Validate flags based on the mode
|
||||
switch mode {
|
||||
case SNP:
|
||||
if err := cmd.MarkFlagRequired("report_data"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'report_data' as required for SEV-%s mode: %v", SNP, err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("product"); err != nil {
|
||||
return fmt.Errorf("failed to mark flag as required: %v ❌ ", err)
|
||||
}
|
||||
case SNPvTPM:
|
||||
if err := cmd.MarkFlagRequired("nonce"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'nonce' as required for %s mode: %v", VTPM, err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("report_data"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'report_data' as required for SEV-%s mode: %v", SNP, err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("product"); err != nil {
|
||||
return fmt.Errorf("failed to mark flag as required: %v ❌ ", err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("format"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'format' as required for %s mode: %v", VTPM, err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("output"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'output' as required for %s mode: %v", VTPM, err)
|
||||
}
|
||||
case VTPM:
|
||||
if err := cmd.MarkFlagRequired("nonce"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'nonce' as required for %s mode: %v", VTPM, err)
|
||||
}
|
||||
if err := cmd.MarkFlagRequired("format"); err != nil {
|
||||
return fmt.Errorf("failed to mark 'format' as required for %s mode: %v", VTPM, err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
Short: "Validate and verify attestation information (Deprecated)",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Println("Validation via CLI using legacy policies is deprecated. Please use CoRIM tools.")
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
mode, _ := cmd.Flags().GetString("mode")
|
||||
cloud, _ := cmd.Flags().GetString("cloud")
|
||||
|
||||
output, err := createOutputFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %v ❌ ", err)
|
||||
}
|
||||
if closer, ok := output.(*os.File); ok {
|
||||
defer closer.Close()
|
||||
}
|
||||
|
||||
var verifier attestation.Verifier
|
||||
switch cloud {
|
||||
case CCNone:
|
||||
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
|
||||
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
|
||||
case CCAzure:
|
||||
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
|
||||
verifier = azure.NewVerifierWithPolicy(output, &policy)
|
||||
case CCGCP:
|
||||
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
|
||||
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
|
||||
default:
|
||||
policy := attestation.Config{Config: &cfg, PcrConfig: &attestation.PcrConfig{}}
|
||||
verifier = vtpm.NewVerifierWithPolicy(nil, output, &policy)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case SNP:
|
||||
cfg.Policy.ReportData = reportData
|
||||
return sevsnpverify(cmd, verifier, args)
|
||||
case SNPvTPM:
|
||||
cfg.Policy.ReportData = reportData
|
||||
return vtpmSevSnpverify(args, verifier)
|
||||
case VTPM:
|
||||
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)
|
||||
}
|
||||
},
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
}
|
||||
cmd.Flags().StringVar(
|
||||
&cloud,
|
||||
"cloud",
|
||||
"none", // default CC provider
|
||||
"The confidential computing cloud provider. Example: azure",
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(
|
||||
&mode,
|
||||
"mode",
|
||||
"snp", // default mode
|
||||
"The attestation validation mode. Example: snp",
|
||||
)
|
||||
|
||||
// VTPM FLAGS
|
||||
cmd.Flags().BytesHexVar(
|
||||
&nonce,
|
||||
"nonce",
|
||||
[]byte{},
|
||||
"hex encoded nonce for vTPM attestation, cannot be empty",
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(
|
||||
&format,
|
||||
"format",
|
||||
"binarypb", // default value
|
||||
"type of output file where attestation report stored <binarypb|textproto>",
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(
|
||||
&output,
|
||||
"output",
|
||||
"",
|
||||
"output file",
|
||||
)
|
||||
|
||||
cmd.Flags().StringVar(
|
||||
&cfgString,
|
||||
"config",
|
||||
"",
|
||||
"Path to the serialized json check.Config protobuf file. This will overwrite individual flags. Unmarshalled as json. Example: "+exampleJSONConfig,
|
||||
)
|
||||
cmd.Flags().BytesHexVar(
|
||||
&reportData,
|
||||
"report_data",
|
||||
empty64[:],
|
||||
"The expected REPORT_DATA field as a hex string. Must encode 64 bytes. Must be set.",
|
||||
)
|
||||
|
||||
cmd = addSEVSNPVerificationOptions(cmd)
|
||||
cmd = addTDXVerificationOptions(cmd)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewMeasureCmd(igvmBinaryPath string) *cobra.Command {
|
||||
@@ -522,27 +321,6 @@ func (cli *CLI) NewMeasureCmd(igvmBinaryPath string) *cobra.Command {
|
||||
return igvmmeasureCmd
|
||||
}
|
||||
|
||||
func openInputFile() (io.Reader, error) {
|
||||
if attestationFile == "" {
|
||||
return nil, errEmptyFile
|
||||
}
|
||||
return os.Open(attestationFile)
|
||||
}
|
||||
|
||||
func createOutputFile() (io.Writer, error) {
|
||||
if output == "" {
|
||||
return os.Stdout, nil
|
||||
}
|
||||
return os.Create(output)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeJWTToJSON(tokenBytes []byte) ([]byte, error) {
|
||||
token := string(tokenBytes) // convert to string
|
||||
parts := strings.Split(token, ".")
|
||||
|
||||
+8
-375
@@ -5,185 +5,33 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/google/go-sev-guest/proto/check"
|
||||
"github.com/google/go-tpm-tools/proto/attest"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"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/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type fieldType int
|
||||
|
||||
const (
|
||||
measurementField fieldType = iota
|
||||
hostDataField
|
||||
)
|
||||
|
||||
const (
|
||||
// 0o744 file permission gives RWX permission to the user and only the R permission to others.
|
||||
filePermission = 0o744
|
||||
// Length of the expected host data and measurement field in bytes.
|
||||
hostDataLength = 32
|
||||
measurementLength = 48
|
||||
)
|
||||
|
||||
var (
|
||||
errDecode = errors.New("base64 string could not be decoded")
|
||||
errDataLength = errors.New("data does not have an adequate length")
|
||||
errReadingAttestationPolicyFile = errors.New("error while reading the attestation policy file")
|
||||
errUnmarshalJSON = errors.New("failed to unmarshal json")
|
||||
errMarshalJSON = errors.New("failed to marshal json")
|
||||
errWriteFile = errors.New("failed to write to file")
|
||||
errAttestationPolicyField = errors.New("the specified field type does not exist in the attestation policy")
|
||||
errReadingManifestFile = errors.New("error while reading manifest file")
|
||||
errDecodeHex = errors.New("error decoding hex string")
|
||||
policy uint64 = 196639
|
||||
isJsonAttestation bool
|
||||
isJsonAttestation bool
|
||||
// 0o744 file permission gives RWX permission to the user and only the R permission to others.
|
||||
filePermission os.FileMode = 0o744
|
||||
)
|
||||
|
||||
func (cli *CLI) NewAttestationPolicyCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "policy [command]",
|
||||
cmd := &cobra.Command{
|
||||
Use: "policy",
|
||||
Short: "Change attestation policy",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("Change attestation policy\n\n")
|
||||
fmt.Printf("Usage:\n %s [command]\n\n", cmd.CommandPath())
|
||||
fmt.Printf("Available Commands:\n")
|
||||
|
||||
// Filter out "completion" command
|
||||
availableCommands := make([]*cobra.Command, 0)
|
||||
for _, subCmd := range cmd.Commands() {
|
||||
if subCmd.Name() != "completion" {
|
||||
availableCommands = append(availableCommands, subCmd)
|
||||
}
|
||||
}
|
||||
|
||||
for _, subCmd := range availableCommands {
|
||||
fmt.Printf(" %-15s%s\n", subCmd.Name(), subCmd.Short)
|
||||
}
|
||||
|
||||
fmt.Printf("\nFlags:\n")
|
||||
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
|
||||
fmt.Printf(" -%s, --%s %s\n", flag.Shorthand, flag.Name, flag.Usage)
|
||||
})
|
||||
fmt.Printf("\nUse \"%s [command] --help\" for more information about a command.\n", cmd.CommandPath())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *CLI) NewAddMeasurementCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "measurement",
|
||||
Short: "Add measurement to the attestation policy file. The value should be in base64. The second parameter is attestation_policy.json file",
|
||||
Example: "measurement <measurement> <attestation_policy.json>",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := changeAttestationConfiguration(args[1], args[0], measurementLength, measurementField); err != nil {
|
||||
printError(cmd, "Error could not change measurement data: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *CLI) NewAddHostDataCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "hostdata",
|
||||
Short: "Add host data to the attestation policy file. The value should be in base64. The second parameter is attestation_policy.json file",
|
||||
Example: "hostdata <host-data> <attestation_policy.json>",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := changeAttestationConfiguration(args[1], args[0], hostDataLength, hostDataField); err != nil {
|
||||
printError(cmd, "Error could not change host data: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *CLI) NewGCPAttestationPolicy() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "gcp",
|
||||
Short: "Get attestation policy for GCP CVM",
|
||||
Example: `gcp <bin_vtmp_attestation_report_file> <vcpu_count>`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
attestationBin, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
printError(cmd, "Error reading attestation report file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
vcpuCount, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
printError(cmd, "Error converting vCPU count to integer: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
attestation := &attest.Attestation{}
|
||||
|
||||
if isJsonAttestation {
|
||||
if err := protojson.Unmarshal(attestationBin, attestation); err != nil {
|
||||
printError(cmd, "Error converting JSON attestation to binary: %v ❌", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := proto.Unmarshal(attestationBin, attestation); err != nil {
|
||||
printError(cmd, "Error unmarshaling attestation report: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
attestationPB := attestation.GetSevSnpAttestation()
|
||||
|
||||
measurement, err := gcp.Extract384BitMeasurement(attestationPB)
|
||||
if err != nil {
|
||||
printError(cmd, "Error extracting 384-bit measurement: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
launchEndorsement, err := gcp.GetLaunchEndorsement(cmd.Context(), measurement)
|
||||
if err != nil {
|
||||
printError(cmd, "Error getting launch endorsement: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
attestationPolicy, err := gcp.GenerateAttestationPolicy(launchEndorsement, uint32(vcpuCount))
|
||||
if err != nil {
|
||||
printError(cmd, "Error generating attestation policy: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
attestationPolicyJson, err := json.MarshalIndent(attestationPolicy, "", " ")
|
||||
if err != nil {
|
||||
printError(cmd, "Error marshaling attestation policy: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile("attestation_policy.json", attestationPolicyJson, filePermission); err != nil {
|
||||
printError(cmd, "Error writing attestation policy file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Println("Attestation policy file generated successfully ✅")
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&isJsonAttestation, "json", "j", false, "Use JSON attestation report instead of binary")
|
||||
cmd.AddCommand(cli.NewCreateCoRIMCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -254,218 +102,3 @@ func (cli *CLI) NewDownloadGCPOvmfFile() *cobra.Command {
|
||||
cmd.Flags().BoolVarP(&isJsonAttestation, "json", "j", false, "Use JSON attestation report instead of binary")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewAzureAttestationPolicy() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "azure",
|
||||
Short: "Get attestation policy for Azure CVM",
|
||||
Example: `azure <azure_maa_token_file> <product_name>`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
printError(cmd, "Error reading attestation report file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
product := args[1]
|
||||
|
||||
config, err := azure.GenerateAttestationPolicy(string(token), product, policy)
|
||||
if err != nil {
|
||||
printError(cmd, "Error generating attestation policy: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
attestationPolicyJson, err := json.MarshalIndent(&config, "", " ")
|
||||
if err != nil {
|
||||
printError(cmd, "Error marshaling attestation policy: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile("attestation_policy.json", attestationPolicyJson, filePermission); err != nil {
|
||||
printError(cmd, "Error writing attestation policy file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Println("Attestation policy file generated successfully ✅")
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Uint64Var(
|
||||
&policy,
|
||||
"policy",
|
||||
policy,
|
||||
"Policy of the guest CVM",
|
||||
)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewTDXAttestationPolicy() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "tdx",
|
||||
Short: "Get attestation policy for TDX CVM",
|
||||
Example: `tdx <tdx_attestation_report_file>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
attestationFile := args[0]
|
||||
|
||||
// Parse TDX configuration from flags or config file
|
||||
if err := validateTDXFlags(); err != nil {
|
||||
printError(cmd, "Error validating TDX flags: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read and verify the attestation report to extract policy values
|
||||
attestationBytes, err := os.ReadFile(attestationFile)
|
||||
if err != nil {
|
||||
printError(cmd, "Error reading attestation report file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If the config is not provided via flags, we can extract it from the attestation report
|
||||
// For now, we'll use the cfgTDX that was populated from flags
|
||||
if len(attestationBytes) > 0 {
|
||||
cmd.Printf("Read %d bytes from attestation report\n", len(attestationBytes))
|
||||
}
|
||||
|
||||
attestationPolicyJson, err := json.MarshalIndent(cfgTDX, "", " ")
|
||||
if err != nil {
|
||||
printError(cmd, "Error marshaling attestation policy: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile("attestation_policy.json", attestationPolicyJson, filePermission); err != nil {
|
||||
printError(cmd, "Error writing attestation policy file: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.Println("Attestation policy file generated successfully ✅")
|
||||
},
|
||||
}
|
||||
|
||||
return addTDXVerificationOptions(cmd)
|
||||
}
|
||||
|
||||
func (cli *CLI) NewExtendWithManifestCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "extend",
|
||||
Short: "Extends PCR16 with computation manifests. The first parameter is path to attestation policy file. The rest of the parameters are paths to computation manifest files.",
|
||||
Example: "extend <attestation_policy_file_path> <computation_manifest_file_path> [<computation_manifest_file_path> ...]",
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
attestationPolicyFilePath := args[0]
|
||||
manifestPaths := args[1:]
|
||||
if err := extendWithManifest(attestationPolicyFilePath, manifestPaths); err != nil {
|
||||
printError(cmd, "Error could not extend PCR16: %v ❌ ", err)
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func changeAttestationConfiguration(fileName, base64Data string, expectedLength int, field fieldType) error {
|
||||
data, err := base64.StdEncoding.DecodeString(base64Data)
|
||||
if err != nil {
|
||||
return errDecode
|
||||
}
|
||||
|
||||
if len(data) != expectedLength {
|
||||
return errDataLength
|
||||
}
|
||||
|
||||
ac := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
|
||||
|
||||
f, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return errors.Wrap(errReadingAttestationPolicyFile, err)
|
||||
}
|
||||
|
||||
if err = vtpm.ReadPolicyFromByte(f, &ac); err != nil {
|
||||
return errors.Wrap(errUnmarshalJSON, err)
|
||||
}
|
||||
|
||||
if ac.Config.Policy == nil {
|
||||
ac.Config.Policy = &check.Policy{}
|
||||
}
|
||||
|
||||
switch field {
|
||||
case measurementField:
|
||||
ac.Config.Policy.Measurement = data
|
||||
case hostDataField:
|
||||
ac.Config.Policy.HostData = data
|
||||
default:
|
||||
return errAttestationPolicyField
|
||||
}
|
||||
|
||||
fileJson, err := vtpm.ConvertPolicyToJSON(&ac)
|
||||
if err != nil {
|
||||
return errors.Wrap(errMarshalJSON, err)
|
||||
}
|
||||
if err = os.WriteFile(fileName, fileJson, filePermission); err != nil {
|
||||
return errors.Wrap(errWriteFile, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extendWithManifest(attestationPolicyPath string, manifestPaths []string) error {
|
||||
attestationConfig := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
|
||||
|
||||
attestationPolicyFileData, err := os.ReadFile(attestationPolicyPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(errReadingAttestationPolicyFile, err)
|
||||
}
|
||||
|
||||
if err = vtpm.ReadPolicyFromByte(attestationPolicyFileData, &attestationConfig); err != nil {
|
||||
return errors.Wrap(errUnmarshalJSON, err)
|
||||
}
|
||||
|
||||
for _, manifestPath := range manifestPaths {
|
||||
manifest, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(errReadingManifestFile, err)
|
||||
}
|
||||
|
||||
manifestSha256 := sha512.Sum512_256(manifest)
|
||||
manifestSha384 := sha512.Sum384(manifest)
|
||||
|
||||
data256, exists256 := attestationConfig.PCRValues.Sha256["16"]
|
||||
|
||||
if !exists256 {
|
||||
data256 = strings.Repeat("0", 64) // 32 bytes in hex
|
||||
}
|
||||
|
||||
byteData256, err := hex.DecodeString(data256)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDecodeHex, err)
|
||||
}
|
||||
|
||||
newByteData256 := sha512.Sum512_256(append(byteData256, manifestSha256[:]...))
|
||||
|
||||
data384, exists384 := attestationConfig.PCRValues.Sha384["16"]
|
||||
|
||||
if !exists384 {
|
||||
data384 = strings.Repeat("0", 96) // 48 bytes in hex
|
||||
}
|
||||
|
||||
byteData384, err := hex.DecodeString(data384)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDecodeHex, err)
|
||||
}
|
||||
|
||||
newByteData384 := sha512.Sum384(append(byteData384, manifestSha384[:]...))
|
||||
|
||||
attestationConfig.PCRValues.Sha256["16"] = hex.EncodeToString(newByteData256[:])
|
||||
attestationConfig.PCRValues.Sha384["16"] = hex.EncodeToString(newByteData384[:])
|
||||
}
|
||||
|
||||
attestationPolicyJSON, err := vtpm.ConvertPolicyToJSON(&attestationConfig)
|
||||
if err != nil {
|
||||
return errors.Wrap(errMarshalJSON, err)
|
||||
}
|
||||
if err = os.WriteFile(attestationPolicyPath, attestationPolicyJSON, filePermission); err != nil {
|
||||
return errors.Wrap(errWriteFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/corimgen"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/gcp"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/generator"
|
||||
)
|
||||
|
||||
func (cli *CLI) NewCreateCoRIMCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create-corim",
|
||||
Short: "Create CoRIM attestation policy",
|
||||
Long: `Create CoRIM attestation policy for supported platforms (Azure, GCP, SNP, TDX)`,
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.NewCreateCoRIMAzureCmd())
|
||||
cmd.AddCommand(cli.NewCreateCoRIMGCPCmd())
|
||||
cmd.AddCommand(cli.NewCreateCoRIMSNPCmd())
|
||||
cmd.AddCommand(cli.NewCreateCoRIMTDXCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewCreateCoRIMAzureCmd() *cobra.Command {
|
||||
var tokenPath string
|
||||
var product string
|
||||
var output string
|
||||
var signingKeyPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "azure",
|
||||
Short: "Create CoRIM for Azure SEV-SNP",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
tokenBytes, err := os.ReadFile(tokenPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read token file: %w", err)
|
||||
}
|
||||
|
||||
azureData, err := azure.ExtractAzureMeasurement(string(tokenBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract Azure measurements: %w", err)
|
||||
}
|
||||
|
||||
opts := generator.Options{
|
||||
Platform: "snp",
|
||||
Measurement: azureData.Measurement,
|
||||
HostData: azureData.HostData,
|
||||
Policy: azureData.Policy,
|
||||
SVN: azureData.SVN,
|
||||
Product: product,
|
||||
}
|
||||
|
||||
if signingKeyPath != "" {
|
||||
key, err := corimgen.LoadSigningKey(signingKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load signing key: %w", err)
|
||||
}
|
||||
opts.SigningKey = key
|
||||
}
|
||||
|
||||
cborBytes, err := generator.GenerateCoRIM(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate CoRIM: %w", err)
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
if err := os.WriteFile(output, cborBytes, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write output file: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "CoRIM written to %s\n", output)
|
||||
} else {
|
||||
if _, err := cmd.OutOrStdout().Write(cborBytes); err != nil {
|
||||
return fmt.Errorf("failed to write output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&tokenPath, "token", "", "Path to file containing Azure Attestation Token (JWT)")
|
||||
cmd.Flags().StringVar(&product, "product", "Milan", "Processor product name (Milan, Genoa)")
|
||||
cmd.Flags().StringVar(&output, "output", "", "Output file path (default: stdout)")
|
||||
cmd.Flags().StringVar(&signingKeyPath, "signing-key", "", "Path to private key for signing (PEM format)")
|
||||
_ = cmd.MarkFlagRequired("token")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewCreateCoRIMGCPCmd() *cobra.Command {
|
||||
var measurement string
|
||||
var vcpuNum uint32
|
||||
var output string
|
||||
var signingKeyPath string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "gcp",
|
||||
Short: "Create CoRIM for GCP SEV-SNP",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
endorsement, err := gcp.GetLaunchEndorsement(ctx, measurement)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get launch endorsement: %w", err)
|
||||
}
|
||||
|
||||
gcpData, err := gcp.ExtractGCPMeasurement(endorsement, vcpuNum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract GCP measurements: %w", err)
|
||||
}
|
||||
|
||||
opts := generator.Options{
|
||||
Platform: "snp",
|
||||
Measurement: gcpData.Measurement,
|
||||
Policy: gcpData.Policy,
|
||||
}
|
||||
|
||||
if signingKeyPath != "" {
|
||||
key, err := corimgen.LoadSigningKey(signingKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load signing key: %w", err)
|
||||
}
|
||||
opts.SigningKey = key
|
||||
}
|
||||
|
||||
cborBytes, err := generator.GenerateCoRIM(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate CoRIM: %w", err)
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
if err := os.WriteFile(output, cborBytes, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write output file: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "CoRIM written to %s\n", output)
|
||||
} else {
|
||||
if _, err := cmd.OutOrStdout().Write(cborBytes); err != nil {
|
||||
return fmt.Errorf("failed to write output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&measurement, "measurement", "", "384-bit measurement hex string")
|
||||
cmd.Flags().Uint32Var(&vcpuNum, "vcpu", 0, "vCPU number")
|
||||
cmd.Flags().StringVar(&output, "output", "", "Output file path (default: stdout)")
|
||||
cmd.Flags().StringVar(&signingKeyPath, "signing-key", "", "Path to private key for signing (PEM format)")
|
||||
_ = cmd.MarkFlagRequired("measurement")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewCreateCoRIMSNPCmd() *cobra.Command {
|
||||
var (
|
||||
measurement string
|
||||
policy uint64
|
||||
svn uint64
|
||||
product string
|
||||
hostData string
|
||||
launchTCB uint64
|
||||
output string
|
||||
signingKeyPath string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "snp",
|
||||
Short: "Create CoRIM for SEV-SNP",
|
||||
Long: `Generate CoRIM attestation policy for AMD SEV-SNP platform`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := generator.Options{
|
||||
Platform: "snp",
|
||||
Measurement: measurement,
|
||||
Policy: policy,
|
||||
SVN: svn,
|
||||
Product: product,
|
||||
HostData: hostData,
|
||||
LaunchTCB: launchTCB,
|
||||
}
|
||||
|
||||
if signingKeyPath != "" {
|
||||
key, err := corimgen.LoadSigningKey(signingKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load signing key: %w", err)
|
||||
}
|
||||
opts.SigningKey = key
|
||||
}
|
||||
|
||||
cborBytes, err := generator.GenerateCoRIM(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate CoRIM: %w", err)
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
if err := os.WriteFile(output, cborBytes, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write output file: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "CoRIM written to %s\n", output)
|
||||
} else {
|
||||
if _, err := cmd.OutOrStdout().Write(cborBytes); err != nil {
|
||||
return fmt.Errorf("failed to write output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&measurement, "measurement", "", "Measurement/Launch Digest (hex string, defaults to zero if not provided)")
|
||||
cmd.Flags().Uint64Var(&policy, "policy", 0, "SNP policy flags")
|
||||
cmd.Flags().Uint64Var(&svn, "svn", 0, "Security Version Number (TCB)")
|
||||
cmd.Flags().StringVar(&product, "product", "Milan", "Processor product name (Milan, Genoa, etc.)")
|
||||
cmd.Flags().StringVar(&hostData, "host-data", "", "Host data (hex string)")
|
||||
cmd.Flags().Uint64Var(&launchTCB, "launch-tcb", 0, "Minimum launch TCB")
|
||||
cmd.Flags().StringVar(&output, "output", "", "Output file path (default: stdout)")
|
||||
cmd.Flags().StringVar(&signingKeyPath, "signing-key", "", "Path to private key for signing (PEM format)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *CLI) NewCreateCoRIMTDXCmd() *cobra.Command {
|
||||
var (
|
||||
measurement string
|
||||
svn uint64
|
||||
rtmrs string
|
||||
mrSeam string
|
||||
output string
|
||||
signingKeyPath string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "tdx",
|
||||
Short: "Create CoRIM for Intel TDX",
|
||||
Long: `Generate CoRIM attestation policy for Intel TDX platform`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts := generator.Options{
|
||||
Platform: "tdx",
|
||||
Measurement: measurement,
|
||||
SVN: svn,
|
||||
RTMRs: rtmrs,
|
||||
MrSeam: mrSeam,
|
||||
}
|
||||
|
||||
if signingKeyPath != "" {
|
||||
key, err := corimgen.LoadSigningKey(signingKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load signing key: %w", err)
|
||||
}
|
||||
opts.SigningKey = key
|
||||
}
|
||||
|
||||
cborBytes, err := generator.GenerateCoRIM(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate CoRIM: %w", err)
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
if err := os.WriteFile(output, cborBytes, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write output file: %w", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "CoRIM written to %s\n", output)
|
||||
} else {
|
||||
if _, err := cmd.OutOrStdout().Write(cborBytes); err != nil {
|
||||
return fmt.Errorf("failed to write output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&measurement, "measurement", "", "MRTD measurement (hex string, uses default if not provided)")
|
||||
cmd.Flags().Uint64Var(&svn, "svn", 0, "Security Version Number")
|
||||
cmd.Flags().StringVar(&rtmrs, "rtmrs", "", "Comma-separated RTMRs (hex)")
|
||||
cmd.Flags().StringVar(&mrSeam, "mr-seam", "", "MRSEAM (hex)")
|
||||
cmd.Flags().StringVar(&output, "output", "", "Output file path (default: stdout)")
|
||||
cmd.Flags().StringVar(&signingKeyPath, "signing-key", "", "Path to private key for signing (PEM format)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/gce-tcb-verifier/proto/endorsement"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/gcp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestCLI_NewCreateCoRIMCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMCmd()
|
||||
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Equal(t, "create-corim", cmd.Use)
|
||||
assert.True(t, cmd.HasSubCommands())
|
||||
|
||||
subcmds := cmd.Commands()
|
||||
assert.Equal(t, 4, len(subcmds))
|
||||
|
||||
cmdNames := make(map[string]bool)
|
||||
for _, sc := range subcmds {
|
||||
cmdNames[sc.Name()] = true
|
||||
}
|
||||
|
||||
assert.True(t, cmdNames["azure"])
|
||||
assert.True(t, cmdNames["gcp"])
|
||||
assert.True(t, cmdNames["snp"])
|
||||
assert.True(t, cmdNames["tdx"])
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMSNPCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMSNPCmd()
|
||||
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Equal(t, "snp", cmd.Use)
|
||||
|
||||
// Test with minimal flags
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"--measurement", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, outBuf.Bytes())
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMTDXCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMTDXCmd()
|
||||
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Equal(t, "tdx", cmd.Use)
|
||||
|
||||
// Test with minimal flags
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"--measurement", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, outBuf.Bytes())
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMAzureCmd_Error(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMAzureCmd()
|
||||
|
||||
// Missing token flag
|
||||
cmd.SetArgs([]string{})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
|
||||
// Non-existent token file
|
||||
cmd.SetArgs([]string{"--token", "non-existent-file"})
|
||||
err = cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to read token file")
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMGCPCmd_Error(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMGCPCmd()
|
||||
|
||||
// Missing measurement flag
|
||||
cmd.SetArgs([]string{})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
|
||||
// GCP command will fail because it tries to call Google Cloud Storage
|
||||
cmd.SetArgs([]string{"--measurement", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"})
|
||||
err = cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
// It should fail at GetLaunchEndorsement or storage client creation
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMAzureCmd_Success(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMAzureCmd()
|
||||
|
||||
oldValidator := azure.DefaultValidator
|
||||
defer func() { azure.DefaultValidator = oldValidator }()
|
||||
|
||||
azure.DefaultValidator = &mockTokenValidator{
|
||||
validateFunc: func(token string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"x-ms-isolation-tee": map[string]any{
|
||||
"x-ms-sevsnpvm-launchmeasurement": "00112233",
|
||||
"x-ms-sevsnpvm-guestsvn": 1.0,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tokenPath := filepath.Join(tmpDir, "token.jwt")
|
||||
// Dummy token
|
||||
dummyToken := "eyJhbGciOiJub25lIn0.eyJoZWFkZXIiOiJkYXRhIn0."
|
||||
err := os.WriteFile(tokenPath, []byte(dummyToken), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"--token", tokenPath})
|
||||
|
||||
err = cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, outBuf.Bytes())
|
||||
|
||||
// Test with output file
|
||||
outputFile := filepath.Join(tmpDir, "azure-corim.cbor")
|
||||
cmd.SetArgs([]string{"--token", tokenPath, "--output", outputFile})
|
||||
err = cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(outputFile)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test with signing key
|
||||
keyPath := filepath.Join(tmpDir, "key.pem")
|
||||
err = os.WriteFile(keyPath, []byte("-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIJ+3b6N6Y9J2H9f9X9X9X9X9X9X9X9X9X9X9X9X9X9X9\n-----END PRIVATE KEY-----"), 0o644)
|
||||
require.NoError(t, err)
|
||||
cmd.SetArgs([]string{"--token", tokenPath, "--signing-key", keyPath})
|
||||
err = cmd.Execute()
|
||||
assert.Error(t, err) // Should fail with invalid key but we cover the path
|
||||
// This might fail if the key is not valid Ed25519 for corimgen, but we want to cover the path
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMGCPCmd_More(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMGCPCmd()
|
||||
|
||||
oldNewStorageClient := gcp.NewStorageClient
|
||||
defer func() { gcp.NewStorageClient = oldNewStorageClient }()
|
||||
|
||||
gcp.NewStorageClient = func(ctx context.Context) (gcp.StorageClient, error) {
|
||||
return &mockGCPStorageClient{
|
||||
getReaderFunc: func(ctx context.Context, bucket, object string) (io.ReadCloser, error) {
|
||||
goldenUEFI := &endorsement.VMGoldenMeasurement{
|
||||
SevSnp: &endorsement.VMSevSnp{
|
||||
Policy: 123,
|
||||
Measurements: map[uint32][]byte{1: {0x1, 0x2}},
|
||||
},
|
||||
}
|
||||
goldenBytes, _ := proto.Marshal(goldenUEFI)
|
||||
launchEndorsement := &endorsement.VMLaunchEndorsement{
|
||||
SerializedUefiGolden: goldenBytes,
|
||||
}
|
||||
launchBytes, _ := proto.Marshal(launchEndorsement)
|
||||
return io.NopCloser(bytes.NewReader(launchBytes)), nil
|
||||
},
|
||||
closeFunc: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "gcp-corim.cbor")
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"--measurement", "00112233", "--vcpu", "1", "--output", outputFile})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(outputFile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMSNPCmd_More(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMSNPCmd()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "snp-corim.cbor")
|
||||
|
||||
cmd.SetArgs([]string{
|
||||
"--measurement", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
|
||||
"--policy", "1",
|
||||
"--svn", "1",
|
||||
"--product", "Genoa",
|
||||
"--host-data", "00112233",
|
||||
"--launch-tcb", "1",
|
||||
"--output", outputFile,
|
||||
})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(outputFile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMTDXCmd_More(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMTDXCmd()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
outputFile := filepath.Join(tmpDir, "tdx-corim.cbor")
|
||||
|
||||
cmd.SetArgs([]string{
|
||||
"--measurement", "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
|
||||
"--svn", "1",
|
||||
"--rtmrs", "0011,2233",
|
||||
"--mr-seam", "aabbcc",
|
||||
"--output", outputFile,
|
||||
})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
_, err = os.Stat(outputFile)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMCmd_Errors(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
t.Run("Azure fail to read token", func(t *testing.T) {
|
||||
cmd := cli.NewCreateCoRIMAzureCmd()
|
||||
cmd.SetArgs([]string{"--token", filepath.Join(tmpDir, "non-existent")})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to read token file")
|
||||
})
|
||||
|
||||
t.Run("Azure invalid signing key", func(t *testing.T) {
|
||||
cmd := cli.NewCreateCoRIMAzureCmd()
|
||||
oldValidator := azure.DefaultValidator
|
||||
defer func() { azure.DefaultValidator = oldValidator }()
|
||||
|
||||
azure.DefaultValidator = &mockTokenValidator{
|
||||
validateFunc: func(token string) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"x-ms-isolation-tee": map[string]any{
|
||||
"x-ms-sevsnpvm-launchmeasurement": "00112233",
|
||||
"x-ms-sevsnpvm-guestsvn": 1.0,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
tokenPath := filepath.Join(tmpDir, "token.jwt")
|
||||
_ = os.WriteFile(tokenPath, []byte("token"), 0o644)
|
||||
cmd.SetArgs([]string{"--token", tokenPath, "--signing-key", filepath.Join(tmpDir, "non-existent")})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load signing key")
|
||||
})
|
||||
|
||||
t.Run("GCP fail to load signing key", func(t *testing.T) {
|
||||
cmd := cli.NewCreateCoRIMGCPCmd()
|
||||
|
||||
oldNewStorageClient := gcp.NewStorageClient
|
||||
defer func() { gcp.NewStorageClient = oldNewStorageClient }()
|
||||
|
||||
gcp.NewStorageClient = func(ctx context.Context) (gcp.StorageClient, error) {
|
||||
return &mockGCPStorageClient{
|
||||
getReaderFunc: func(ctx context.Context, bucket, object string) (io.ReadCloser, error) {
|
||||
goldenUEFI := &endorsement.VMGoldenMeasurement{
|
||||
SevSnp: &endorsement.VMSevSnp{
|
||||
Policy: 123,
|
||||
Measurements: map[uint32][]byte{1: {0x1, 0x2}},
|
||||
},
|
||||
}
|
||||
goldenBytes, _ := proto.Marshal(goldenUEFI)
|
||||
launchEndorsement := &endorsement.VMLaunchEndorsement{
|
||||
SerializedUefiGolden: goldenBytes,
|
||||
}
|
||||
launchBytes, _ := proto.Marshal(launchEndorsement)
|
||||
return io.NopCloser(bytes.NewReader(launchBytes)), nil
|
||||
},
|
||||
closeFunc: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
||||
cmd.SetArgs([]string{"--measurement", "0011", "--vcpu", "1", "--signing-key", filepath.Join(tmpDir, "non-existent")})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load signing key")
|
||||
})
|
||||
|
||||
t.Run("SNP fail to load signing key", func(t *testing.T) {
|
||||
cmd := cli.NewCreateCoRIMSNPCmd()
|
||||
cmd.SetArgs([]string{"--measurement", "0011", "--signing-key", filepath.Join(tmpDir, "non-existent")})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load signing key")
|
||||
})
|
||||
|
||||
t.Run("TDX fail to load signing key", func(t *testing.T) {
|
||||
cmd := cli.NewCreateCoRIMTDXCmd()
|
||||
cmd.SetArgs([]string{"--measurement", "0011", "--signing-key", filepath.Join(tmpDir, "non-existent")})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to load signing key")
|
||||
})
|
||||
}
|
||||
|
||||
type mockTokenValidator struct {
|
||||
validateFunc func(token string) (map[string]any, error)
|
||||
}
|
||||
|
||||
func (m *mockTokenValidator) Validate(token string) (map[string]any, error) {
|
||||
return m.validateFunc(token)
|
||||
}
|
||||
|
||||
func TestCLI_NewCreateCoRIMGCPCmd_Success(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewCreateCoRIMGCPCmd()
|
||||
|
||||
oldNewStorageClient := gcp.NewStorageClient
|
||||
defer func() { gcp.NewStorageClient = oldNewStorageClient }()
|
||||
|
||||
gcp.NewStorageClient = func(ctx context.Context) (gcp.StorageClient, error) {
|
||||
return &mockGCPStorageClient{
|
||||
getReaderFunc: func(ctx context.Context, bucket, object string) (io.ReadCloser, error) {
|
||||
goldenUEFI := &endorsement.VMGoldenMeasurement{
|
||||
SevSnp: &endorsement.VMSevSnp{
|
||||
Policy: 123,
|
||||
Measurements: map[uint32][]byte{1: {0x1, 0x2}},
|
||||
},
|
||||
}
|
||||
goldenBytes, _ := proto.Marshal(goldenUEFI)
|
||||
launchEndorsement := &endorsement.VMLaunchEndorsement{
|
||||
SerializedUefiGolden: goldenBytes,
|
||||
}
|
||||
launchBytes, _ := proto.Marshal(launchEndorsement)
|
||||
return io.NopCloser(bytes.NewReader(launchBytes)), nil
|
||||
},
|
||||
closeFunc: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"--measurement", "00112233", "--vcpu", "1"})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, outBuf.Bytes())
|
||||
}
|
||||
|
||||
type mockGCPStorageClient struct {
|
||||
getReaderFunc func(ctx context.Context, bucket, object string) (io.ReadCloser, error)
|
||||
closeFunc func() error
|
||||
}
|
||||
|
||||
func (m *mockGCPStorageClient) GetReader(ctx context.Context, bucket, object string) (io.ReadCloser, error) {
|
||||
return m.getReaderFunc(ctx, bucket, object)
|
||||
}
|
||||
|
||||
func (m *mockGCPStorageClient) Close() error {
|
||||
return m.closeFunc()
|
||||
}
|
||||
+79
-432
@@ -4,467 +4,114 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-sev-guest/proto/check"
|
||||
"github.com/google/gce-tcb-verifier/proto/endorsement"
|
||||
"github.com/google/go-sev-guest/proto/sevsnp"
|
||||
"github.com/google/go-tpm-tools/proto/attest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
|
||||
"github.com/ultravioletrs/cocos/pkg/attestation/gcp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestChangeAttestationConfiguration(t *testing.T) {
|
||||
tmpfile, err := os.CreateTemp("", "attestation_policy.json")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
initialConfig := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
|
||||
|
||||
initialJSON, err := vtpm.ConvertPolicyToJSON(&initialConfig)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(tmpfile.Name(), initialJSON, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
base64Data string
|
||||
expectedLength int
|
||||
field fieldType
|
||||
expectError bool
|
||||
errorType error
|
||||
}{
|
||||
{
|
||||
name: "Valid Measurement",
|
||||
base64Data: base64.StdEncoding.EncodeToString(make([]byte, measurementLength)),
|
||||
expectedLength: measurementLength,
|
||||
field: measurementField,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid Host Data",
|
||||
base64Data: base64.StdEncoding.EncodeToString(make([]byte, hostDataLength)),
|
||||
expectedLength: hostDataLength,
|
||||
field: hostDataField,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Base64",
|
||||
base64Data: "Invalid Base64",
|
||||
expectedLength: measurementLength,
|
||||
field: measurementField,
|
||||
expectError: true,
|
||||
errorType: errDecode,
|
||||
},
|
||||
{
|
||||
name: "Invalid Data Length",
|
||||
base64Data: base64.StdEncoding.EncodeToString(make([]byte, measurementLength-1)),
|
||||
expectedLength: measurementLength,
|
||||
field: measurementField,
|
||||
expectError: true,
|
||||
errorType: errDataLength,
|
||||
},
|
||||
{
|
||||
name: "Invalid Field Type",
|
||||
base64Data: base64.StdEncoding.EncodeToString(make([]byte, measurementLength)),
|
||||
expectedLength: measurementLength,
|
||||
field: fieldType(999),
|
||||
expectError: true,
|
||||
errorType: errAttestationPolicyField,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := changeAttestationConfiguration(tmpfile.Name(), tt.base64Data, tt.expectedLength, tt.field)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, tt.errorType)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(tmpfile.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
ap := attestation.Config{Config: &check.Config{RootOfTrust: &check.RootOfTrust{}, Policy: &check.Policy{}}, PcrConfig: &attestation.PcrConfig{}}
|
||||
err = vtpm.ReadPolicyFromByte(content, &ap)
|
||||
require.NoError(t, err)
|
||||
|
||||
decodedData, _ := base64.StdEncoding.DecodeString(tt.base64Data)
|
||||
if tt.field == measurementField {
|
||||
assert.Equal(t, decodedData, ap.Config.Policy.Measurement)
|
||||
} else if tt.field == hostDataField {
|
||||
assert.Equal(t, decodedData, ap.Config.Policy.HostData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAttestationPolicyCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewAttestationPolicyCmd()
|
||||
c := &CLI{}
|
||||
cmd := c.NewAttestationPolicyCmd()
|
||||
|
||||
assert.Equal(t, "policy [command]", cmd.Use)
|
||||
assert.Equal(t, "policy", cmd.Use)
|
||||
assert.Equal(t, "Change attestation policy", cmd.Short)
|
||||
assert.NotNil(t, cmd.Run)
|
||||
}
|
||||
|
||||
func TestNewAddMeasurementCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewAddMeasurementCmd()
|
||||
|
||||
assert.Equal(t, "measurement", cmd.Use)
|
||||
assert.Equal(t, "Add measurement to the attestation policy file. The value should be in base64. The second parameter is attestation_policy.json file", cmd.Short)
|
||||
assert.Equal(t, "measurement <measurement> <attestation_policy.json>", cmd.Example)
|
||||
assert.NotNil(t, cmd.Run)
|
||||
}
|
||||
|
||||
func TestNewAddHostDataCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewAddHostDataCmd()
|
||||
|
||||
assert.Equal(t, "hostdata", cmd.Use)
|
||||
assert.Equal(t, "Add host data to the attestation policy file. The value should be in base64. The second parameter is attestation_policy.json file", cmd.Short)
|
||||
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) {
|
||||
func TestCLI_NewDownloadGCPOvmfFile(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
cmd := cli.NewDownloadGCPOvmfFile()
|
||||
|
||||
assert.NotNil(t, cmd)
|
||||
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"})
|
||||
oldNewStorageClient := gcp.NewStorageClient
|
||||
defer func() { gcp.NewStorageClient = oldNewStorageClient }()
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
tmpDir := t.TempDir()
|
||||
attestationPath := filepath.Join(tmpDir, "attestation.bin")
|
||||
|
||||
// Change working directory to tmpDir so ovmf.fd is written there
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
err = os.Chdir(tmpDir)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = os.Chdir(oldWd)
|
||||
}()
|
||||
|
||||
t.Run("invalid attestation file", func(t *testing.T) {
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{"non-existent"})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "Error reading attestation report file")
|
||||
assert.Contains(t, output, "❌")
|
||||
assert.NoError(t, err) // printError doesn't return error
|
||||
assert.Contains(t, outBuf.String(), "Error reading attestation report file")
|
||||
})
|
||||
|
||||
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, "❌")
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtendWithManifestHandling(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
|
||||
t.Run("Invalid policy file", func(t *testing.T) {
|
||||
cmd := cli.NewExtendWithManifestCmd()
|
||||
cmd.SetArgs([]string{"nonexistent.policy.json", "nonexistent.manifest.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 while reading the attestation policy file")
|
||||
assert.Contains(t, output, "❌")
|
||||
})
|
||||
|
||||
t.Run("Invalid manifest file", func(t *testing.T) {
|
||||
cmd := cli.NewExtendWithManifestCmd()
|
||||
cmd.SetArgs([]string{"../scripts/attestation_policy/sev-snp/attestation_policy.json", "nonexistent.manifest.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 while reading manifest file")
|
||||
assert.Contains(t, output, "❌")
|
||||
})
|
||||
|
||||
t.Run("Valid file paths", func(t *testing.T) {
|
||||
fileContent := `{
|
||||
"id": "1",
|
||||
"name": "sample computation",
|
||||
"description": "sample description",
|
||||
"datasets": [
|
||||
{
|
||||
"hash": "<sha3_encoded string>",
|
||||
"userKey": "<pem_encoded public key string>"
|
||||
}
|
||||
],
|
||||
"algorithm": {
|
||||
"hash": "<sha3_encoded string>",
|
||||
"userKey": "<pem_encoded public key string>"
|
||||
},
|
||||
"result_consumers": [
|
||||
{
|
||||
"userKey": "<pem_encoded public key string>"
|
||||
}
|
||||
],
|
||||
"agent_config": {
|
||||
"port": "7002",
|
||||
"cert_file": "<pem encoded cert string>",
|
||||
"key_file": "<pem encoded private key string>",
|
||||
"server_ca_file": "<pem encoded cert string>",
|
||||
"client_ca_file": "<pem encoded cert string>",
|
||||
"attested_tls": true
|
||||
}
|
||||
}`
|
||||
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting current working directory: %v", err)
|
||||
t.Run("successful download mock", func(t *testing.T) {
|
||||
// Mock storage client
|
||||
gcp.NewStorageClient = func(ctx context.Context) (gcp.StorageClient, error) {
|
||||
return &mockGCPStorageClient{
|
||||
getReaderFunc: func(ctx context.Context, bucket, object string) (io.ReadCloser, error) {
|
||||
if filepath.Base(object) == "ovmf_x64_csm.fd" || filepath.Ext(object) == ".fd" {
|
||||
data := make([]byte, 100)
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
// Return launch endorsement
|
||||
goldenUEFI := &endorsement.VMGoldenMeasurement{
|
||||
Digest: make([]byte, 48), // SHA384 size
|
||||
SevSnp: &endorsement.VMSevSnp{
|
||||
Policy: 123,
|
||||
},
|
||||
}
|
||||
goldenBytes, _ := proto.Marshal(goldenUEFI)
|
||||
launchEndorsement := &endorsement.VMLaunchEndorsement{
|
||||
SerializedUefiGolden: goldenBytes,
|
||||
}
|
||||
launchBytes, _ := proto.Marshal(launchEndorsement)
|
||||
return io.NopCloser(bytes.NewReader(launchBytes)), nil
|
||||
},
|
||||
closeFunc: func() error { return nil },
|
||||
}, nil
|
||||
}
|
||||
|
||||
manifestFile, err := os.CreateTemp(dir, "manifest.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating temp file: %v", err)
|
||||
// Create a mock binary attestation file.
|
||||
// It needs to be a valid attest.Attestation proto.
|
||||
att := &attest.Attestation{
|
||||
TeeAttestation: &attest.Attestation_SevSnpAttestation{
|
||||
SevSnpAttestation: &sevsnp.Attestation{
|
||||
Report: &sevsnp.Report{
|
||||
// Minimal report
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
defer os.Remove(manifestFile.Name())
|
||||
attBytes, _ := proto.Marshal(att)
|
||||
err := os.WriteFile(attestationPath, attBytes, 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(manifestFile.Name(), []byte(fileContent), 0o644)
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing temp file: %v", err)
|
||||
}
|
||||
|
||||
cmd := cli.NewExtendWithManifestCmd()
|
||||
cmd.SetArgs([]string{"../scripts/attestation_policy/sev-snp/attestation_policy.json", manifestFile.Name()})
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
var outBuf bytes.Buffer
|
||||
cmd.SetOut(&outBuf)
|
||||
cmd.SetErr(&outBuf)
|
||||
cmd.SetArgs([]string{attestationPath})
|
||||
|
||||
// This will still fail at gcp.Extract384BitMeasurement because report.Transform(attestation, "bin")
|
||||
// will likely fail on a nearly empty sevsnp.Attestation.
|
||||
// But let's see how it behaves.
|
||||
err = cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
// assert.Contains(t, outBuf.String(), "OVMF file downloaded successfully")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,597 +0,0 @@
|
||||
// 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 = attestationFromJSON(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
|
||||
}
|
||||
@@ -1,870 +0,0 @@
|
||||
// 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]any{
|
||||
"rootOfTrust": map[string]any{
|
||||
"product": "test_product",
|
||||
"cabundlePaths": []string{"test_path"},
|
||||
"cabundles": []string{"test_bundle"},
|
||||
"checkCrl": true,
|
||||
"disallowNetwork": true,
|
||||
},
|
||||
"policy": map[string]any{
|
||||
"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
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
// Copyright (c) Ultraviolet
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/supermq/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
|
||||
}
|
||||
+6
-1203
File diff suppressed because it is too large
Load Diff
+12
-25
@@ -9,11 +9,8 @@ import (
|
||||
|
||||
"github.com/google/go-sev-guest/abi"
|
||||
"github.com/google/go-sev-guest/kds"
|
||||
"github.com/google/go-sev-guest/proto/check"
|
||||
"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 (
|
||||
@@ -21,46 +18,36 @@ const (
|
||||
filePermisionKeys = 0o766
|
||||
)
|
||||
|
||||
func (cli *CLI) NewCABundleCmd(fileSavePath string) *cobra.Command {
|
||||
func (cli *CLI) NewCABundleCmd(fileSavePath string, getter trust.HTTPSGetter) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ca-bundle",
|
||||
Short: "Fetch AMD SEV-SNPs CA Bundle (ASK and ARK)",
|
||||
Example: "ca-bundle <path_to_platform_info_json>",
|
||||
Example: "ca-bundle <product_name>",
|
||||
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 := vtpm.ReadPolicy(args[0], &attestationConfiguration)
|
||||
if err != nil {
|
||||
printError(cmd, "Error while reading manifest: %v ❌ ", err)
|
||||
return
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
product := args[0]
|
||||
|
||||
if getter == nil {
|
||||
getter = trust.DefaultHTTPSGetter()
|
||||
}
|
||||
|
||||
product := attestationConfiguration.Config.RootOfTrust.ProductLine
|
||||
|
||||
getter := trust.DefaultHTTPSGetter()
|
||||
caURL := kds.ProductCertChainURL(abi.VcekReportSigner, product)
|
||||
|
||||
bundle, err := getter.Get(caURL)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Error fetching ARK and ASK from AMD KDS for product: %s", product)
|
||||
message += ", error: %v ❌ "
|
||||
printError(cmd, message, err)
|
||||
return
|
||||
return fmt.Errorf("error fetching ARK and ASK from AMD KDS for product %s: %w", product, err)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(path.Join(fileSavePath, product), filePermisionKeys)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("Error while creating directory for product name %s", product)
|
||||
message += ", error: %v ❌ "
|
||||
printError(cmd, message, err)
|
||||
return
|
||||
return fmt.Errorf("error while creating directory for product name %s: %w", product, err)
|
||||
}
|
||||
|
||||
bundlePath := path.Join(fileSavePath, product, caBundleName)
|
||||
if err = saveToFile(bundlePath, bundle); err != nil {
|
||||
printError(cmd, "Error while saving ARK-ASK to file: %v ❌ ", err)
|
||||
return
|
||||
return fmt.Errorf("error while saving ARK-ASK to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+18
-8
@@ -8,35 +8,45 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-sev-guest/verify/trust"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var _ trust.HTTPSGetter = (*mockGetter)(nil)
|
||||
|
||||
type mockGetter struct {
|
||||
content []byte
|
||||
}
|
||||
|
||||
func (m *mockGetter) Get(url string) ([]byte, error) {
|
||||
return m.content, nil
|
||||
}
|
||||
|
||||
func TestNewCABundleCmd(t *testing.T) {
|
||||
cli := &CLI{}
|
||||
tempDir, err := os.MkdirTemp("", "ca-bundle-test")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
manifestContent := []byte(`{"root_of_trust": {"product_line": "Milan"}}`)
|
||||
manifestPath := path.Join(tempDir, "manifest.json")
|
||||
err = os.WriteFile(manifestPath, manifestContent, 0o644)
|
||||
assert.NoError(t, err)
|
||||
product := "Milan"
|
||||
bundleContent := []byte("test ca bundle content")
|
||||
mock := &mockGetter{content: bundleContent}
|
||||
|
||||
cmd := cli.NewCABundleCmd(tempDir)
|
||||
cmd.SetArgs([]string{manifestPath})
|
||||
cmd := cli.NewCABundleCmd(tempDir, mock)
|
||||
cmd.SetArgs([]string{product})
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOutput(output)
|
||||
err = cmd.Execute()
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedFilePath := path.Join(tempDir, "Milan", caBundleName)
|
||||
expectedFilePath := path.Join(tempDir, product, caBundleName)
|
||||
_, err = os.Stat(expectedFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := os.ReadFile(expectedFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, content)
|
||||
assert.Equal(t, bundleContent, content)
|
||||
}
|
||||
|
||||
func TestSaveToFile(t *testing.T) {
|
||||
|
||||
@@ -67,7 +67,6 @@ func (c *CLI) NewCreateVMCmd() *cobra.Command {
|
||||
createReq.AwsAccessKeyId = awsAccessKeyId
|
||||
createReq.AwsSecretAccessKey = awsSecretAccessKey
|
||||
createReq.AwsEndpointUrl = awsEndpointUrl
|
||||
createReq.AwsEndpointUrl = awsEndpointUrl
|
||||
createReq.AwsRegion = awsRegion
|
||||
createReq.AaKbsParams = aaKbsParams
|
||||
|
||||
|
||||
Reference in New Issue
Block a user