Compare commits

...

12 Commits

Author SHA1 Message Date
dependabot[bot] d7f5118d5d Bump github.com/fxamacker/cbor/v2 from 2.9.0 to 2.9.2
Bumps [github.com/fxamacker/cbor/v2](https://github.com/fxamacker/cbor) from 2.9.0 to 2.9.2.
- [Release notes](https://github.com/fxamacker/cbor/releases)
- [Commits](https://github.com/fxamacker/cbor/compare/v2.9.0...v2.9.2)

---
updated-dependencies:
- dependency-name: github.com/fxamacker/cbor/v2
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-15 20:53:31 +00:00
Sammy Kerata Oina 6169766666 NOISSUE - Fix agent startup issues (#605)
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
* Update attestationFromCert function to include ccPlatform parameter for enhanced attestation processing

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

* chore: migrate dependencies from supermq to magistrala and update build configurations

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

* chore: update project dependencies, repository source, and support TDX QuoteV5 attestation

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

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2026-06-11 17:08:24 +02:00
Sammy Kerata Oina 5f339d2fab NOISSUE - Refactor test functions to use testing.TB interface and enhance SNP claims extraction logic (#599)
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
* Refactor test functions to use testing.TB interface and enhance SNP claims extraction logic

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: Danko Miladinovic <72250944+danko-miladinovic@users.noreply.github.com>
Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Enhance VTPM claims extraction logic and update test to use proto.Marshal for report generation

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

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: Danko Miladinovic <72250944+danko-miladinovic@users.noreply.github.com>
2026-06-08 17:29:04 +02:00
dependabot[bot] 7e8eab77e7 Bump cloud.google.com/go/storage from 1.57.2 to 1.62.3 (#581)
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
Bumps [cloud.google.com/go/storage](https://github.com/googleapis/google-cloud-go) from 1.57.2 to 1.62.3.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/storage/v1.57.2...storage/v1.62.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:28:37 +03:00
dependabot[bot] 9f31e2472b Bump github.com/absmach/supermq from 0.19.0 to 0.19.1 (#580)
Bumps [github.com/absmach/supermq](https://github.com/absmach/supermq) from 0.19.0 to 0.19.1.
- [Commits](https://github.com/absmach/supermq/commits/v0.19.1)

---
updated-dependencies:
- dependency-name: github.com/absmach/supermq
  dependency-version: 0.19.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:18:16 +03:00
dependabot[bot] e8e616ff62 Bump github.com/absmach/certs from 0.18.2 to 0.18.5 (#574)
Bumps [github.com/absmach/certs](https://github.com/absmach/certs) from 0.18.2 to 0.18.5.
- [Release notes](https://github.com/absmach/certs/releases)
- [Commits](https://github.com/absmach/certs/compare/v0.18.2...v0.18.5)

---
updated-dependencies:
- dependency-name: github.com/absmach/certs
  dependency-version: 0.18.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:07:54 +03:00
dependabot[bot] 0dce9d3083 Bump github.com/google/go-tpm from 0.9.6 to 0.9.8 (#572)
Bumps [github.com/google/go-tpm](https://github.com/google/go-tpm) from 0.9.6 to 0.9.8.
- [Release notes](https://github.com/google/go-tpm/releases)
- [Commits](https://github.com/google/go-tpm/compare/v0.9.6...v0.9.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:07:36 +03:00
dependabot[bot] a37121dc7b Bump github.com/go-chi/chi/v5 from 5.2.3 to 5.2.4 (#570)
Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.2.3 to 5.2.4.
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.2.4)

---
updated-dependencies:
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-04 15:07:17 +03:00
Jovan Djukic 1f0eccfae7 Manager can start CVM with NVIDIA GPU support (#595)
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
2026-05-25 12:32:00 +02:00
Danko Miladinovic 02aa7d7d85 NOISSUE - Azure TDX Support (#596)
* initial Azure TDX support

* add tests

* update documentation

---------

Co-authored-by: Ubuntu <danko@cocos.nbzvzgavv4yeximq0jorvcggfd.dx.internal.cloudapp.net>
2026-05-25 12:22:29 +02:00
Jovan Djukic 27db9b29eb COCOS-591: Add support for GPU CC attestation (#592)
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
* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* NOISSUE - Enforce binding label check (#589)

* NOISSUE - Implement extensible resource downloader framework with support for S3, GCS, and OCI sources (#590)

* feat: implement extensible resource downloader framework with support for S3, GCS, and OCI sources

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

* refactor: improve resource URL parsing and add support for bare OCI image references

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

* fix: add empty string check and slash requirement for OCI image inference, and update python unit tests with event mock expectations

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

* refactor: introduce OCIClient interface, add test coverage for decryption, and improve resource download error handling

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

* chore: remove trailing whitespace in OCI downloader and HTTP tests

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

---------

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

* Refactored baed on comments

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* fixed lint error

* fixed tests

* Fixed according to comments

* COCOS-584 - Support multiple kbs (#587)

* feat: Implement per-resource KBS configuration, allowing algorithms and datasets to specify individual KBS URLs.

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

* refactor: Encapsulate CLI error handling and CVM certificate paths within the CLI struct, and add algorithm type to agent's algorithm structure.

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

* style: Remove blank lines and fix indentation in CLI commands.

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

* refactor: Update downloadAndDecryptGenericResource to accept KBS URL as a parameter and adjust related tests

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

* refactor: group CLI configuration into structured types and simplify skopeo decryption key handling

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

---------

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

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* Added GPU evidence collection

* Added GPU evidence verification

* Added make command for nvattest helper

* Added command for installing all services

* changed attestion-service.service so it knows where the helper is

* Possible IGVM script bug

* Possible bug

* Bug

* bug

* Revert "bug"

This reverts commit d81d67e73d.

* Revert "Bug"

This reverts commit 5e566d53c1.

* Revert "Possible bug"

This reverts commit 47d13fe583.

* Revert "Possible IGVM script bug"

This reverts commit 3fb1b79537.

* Revert "changed attestion-service.service so it knows where the helper is"

This reverts commit f9f11ed183.

* Revert "Added command for installing all services"

This reverts commit 5dcf7a5c0a.

* Refactored baed on comments

* fixed lint error

* fixed tests

* Fixed according to comments

---------

Signed-off-by: SammyOina <sammyoina@gmail.com>
Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: Danko Miladinovic <72250944+danko-miladinovic@users.noreply.github.com>
Co-authored-by: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com>
2026-05-08 16:35:04 +02:00
Danko Miladinovic 81fe0b11b5 COCOS-432 - FDE support (#553)
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
* initial FDE setup

* add Manager support

* fix igvmmeasure build

* rebase on main

* add tests

* NOISSUE - Allow interoperability with CC Attestation Agent (#568)

* feat: Add Confidential Containers attestation agent as an alternative attestation backend with new proto definitions and build system integration.

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

* fix: Update protoc-gen-go and protoc-gen-go-grpc versions in CI workflow

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

* feat: Add mock implementation for AttestationAgentServiceClient and corresponding tests

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

* fix: Add missing periods to test function comments in provider_test.go

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

---------

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

* NOISSUE - Agent Pull mode for remote resources (#575)

* feat(kbs): implement KBS client for attestation and resource retrieval

- Added KBS client implementation in pkg/kbs/client.go with methods for attestation and resource retrieval.
- Introduced necessary data structures for requests and responses.
- Implemented error handling for various scenarios.

test(kbs): add unit tests for KBS client

- Created comprehensive tests for the KBS client in pkg/kbs/client_test.go.
- Included tests for attestation success and failure cases, as well as resource retrieval.

feat(registry): introduce HTTP and S3 registry implementations

- Added HTTPRegistry for downloading resources over HTTP/HTTPS with retry logic in pkg/registry/http.go.
- Implemented S3Registry for downloading resources from AWS S3 and S3-compatible services in pkg/registry/s3.go.
- Included error handling and configuration options for both registries.

chore(registry): define registry interface and configuration

- Created registry interface and configuration struct in pkg/registry/registry.go.
- Added default configuration settings for registry clients.

docs(cvms): update README for CVMS server configuration and usage

- Enhanced documentation for CVMS server with detailed command-line flags and usage examples.
- Clarified direct upload and remote resource modes, including KBS integration.

fix(cvms): integrate KBS for remote resource handling in main.go

- Updated main.go to support remote datasets and algorithms using KBS.
- Added validation for command-line flags to ensure proper configuration.

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

* fix: Move ifeq conditional outside define block in attestation-service.mk

Make conditionals cannot be evaluated inside define...endef blocks
when used as recipe bodies. Restructured to define the
ATTESTATION_SERVICE_INSTALL_INIT_SYSTEMD block conditionally based
on BR2_PACKAGE_CC_ATTESTATION_AGENT configuration.

* feat: Implement remote resource downloading for algorithms and datasets using AWS S3/MinIO credentials.

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

* feat: Add comprehensive documentation and agent support for testing remote resource download with KBS attestation.

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

* feat: Improve agent logging for remote resource configuration and KBS status, and add a testing guide for remote resource downloads with KBS attestation.

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

* feat: Add a comprehensive guide for testing remote resource download with KBS attestation and update multiple package versions to a specific commit.

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

* feat: Add failure transitions for resource reception states and a comprehensive guide for testing remote resource downloads with KBS attestation.

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

* feat: Implement remote resource download with KBS attestation in the agent and add a comprehensive testing guide.

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

* test: Add comprehensive guide for testing remote resource download with KBS attestation and include a debug log in the attestation client.

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

* feat: Delegate KBS attestation and token retrieval to a new attestation-agent service and document remote resource testing.

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

* client fixes

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

* raw evidence

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

* fix: Build all Go files in cmd directories, not just main.go

This fixes the issue where fetch_raw_evidence.go wasn't being included
in the attestation-service build.

* fix: Wrap binary evidence in JSON for KBS compatibility

Fixes 'invalid character' error by wrapping raw binary evidence
in a JSON structure with base64 encoding, as expected by KBS.

* chore: Update buildroot packages to c28cefae

Includes fixes for:
1. attestation-service build (including fetch_raw_evidence.go)
2. Agent KBS evidence format (wrapping binary in JSON)

* fix: Implement KBS RCAR handshake with cookies

Fixes 'cookie not found' error (401) from KBS by:
1. Adding CookieJar support to KBS client
2. Implementing GetChallenge() to perform /auth handshake and capture session cookie
3. Updating Agent to get challenge, decode nonce, and use it for evidence generation
4. Regenerating mocks

* chore: Update buildroot packages to f6981ac5

Includes KBS RCAR handshake fix (cookie support + GetChallenge loop)

* fix: Update KBS client JSON tags to kebab-case

Fixes deserialization error (401) from KBS by:
1. Using kebab-case (e.g. extra-params) for JSON tags as per protocol.
2. Initializing ExtraParams as empty object {} instead of null/omitted.

* fix: Wrap attestation evidence in primary_evidence format

Updates Agent to construct 'tee-evidence' payload with:
- primary_evidence: containing the actual quote/data
- additional_evidence: empty JSON object

This matches the Confidential Containers KBS Attestation Protocol requirements.

* fix: Update KBS protocol version to 0.4.0

KBS rejected 0.1.0 with a version mismatch error. Bumping to 0.4.0 to match server expectation.

* fix: Generate ephemeral key for KBS RuntimeData

Updates RuntimeData to include a valid ephemeral EC P-256 public key in JWK format, as required by the KBS RCAR protocol.
Also fixes the KBS client struct to support TEEPubKey as an object.

* fix: Update sample attestation quote to valid JSON

The default attestation.bin was binary, but the KBS Sample Verifier expects a valid JSON quote containing 'svn' and 'report_data'.
Updated the embedded bin file to contain this JSON structure.

* fix: Generate dynamic JSON quote for Sample TEE in FetchRawEvidence

The KBS Sample Verifier expects a JSON object with 'svn' and 'report_data'.
Previously, we were returning raw binary data (reportData+nonce).
This commit updates FetchRawEvidence to return a marshaled JSON structure with:
- svn: "1"
- report_data: base64(req.ReportData)

* refactor: Delegate Sample Attestation to Provider

Refactored sample attestation logic:
- Moved JSON Quote generation into EmptyProvider (standalone mode).
- Updated FetchRawEvidence to call provider.TeeAttestation instead of manual generation.
This enables using the real CC Attestation Agent for UNSPECIFIED platform if configured.

* feat: Add comprehensive debug logging and enforce CC AA usage

Changes:
- Updated EmptyProvider to return error instead of generating mock data
  This forces proper use of CC Attestation Agent's sample attester
- Added detailed logging to attestation-service FetchRawEvidence:
  * Hex dump of evidence (first 200 bytes)
  * String preview of evidence
  * Total evidence length
- Added detailed logging to agent service:
  * Raw evidence hex and string previews
  * KBS evidence JSON preview (first 500 bytes)
  * Evidence lengths at each transformation step

This logging will help diagnose why KBS Sample Verifier is rejecting evidence.

* fix: Enable CC AA by default and add attestation-service log forwarding

Changes:
- Set USE_CC_ATTESTATION_AGENT=true by default in systemd service
- Added StandardOutput/StandardError to forward logs to /var/log/cocos/
- Updated HAL makefile to handle new default value
- This ensures attestation-service uses CC AA's sample attester
- Logs will now be visible in CVMS output for debugging

* feat: Add gRPC log forwarding to attestation-service

Implemented the same log forwarding mechanism used by the agent:
- Added ProtoHandler to write logs to both stdout and logQueue
- Connected to log client (/run/cocos/log.sock) for gRPC forwarding
- Added goroutine to forward logs to CVMS via log client
- Logs will now appear in CVMS output during computation runs

This enables visibility into attestation-service debug output including:
- CC AA connection status
- Evidence generation details (hex dumps, string previews)
- Any errors from providers

* fix: Parse sample evidence JSON instead of base64-encoding it

The attestation-service returns sample evidence as JSON:
{"svn":"1","report_data":"base64..."}

The agent was incorrectly base64-encoding this JSON string again.
KBS Sample Verifier expects the parsed JSON object directly.

Fixed by:
- Parsing the JSON evidence from attestation-service
- Passing the parsed object directly in primary_evidence.evidence
- This matches what KBS Sample Verifier expects

* debug: Increase KBS evidence logging preview to 1000 bytes

Show the complete JSON structure being sent to KBS to debug
the attestation failure.

* debug: Add comprehensive CC AA configuration logging

Added debug logs to show:
- Whether CC AA is enabled in config
- CC AA address being used
- Connection success/failure
- Which provider is ultimately selected
- Warning when falling back to EmptyProvider

This will help diagnose why EmptyProvider is being used
instead of CC Attestation Agent.

* debug: Add startup logging for log client connection

Added log message to show if log client connection succeeds
at attestation-service startup. This will help diagnose why
logs aren't appearing in CVMS output.

* feat: Add retry logic with exponential backoff to log client

Added simple retry mechanism to handle concurrent log requests:
- 3 retry attempts with exponential backoff (10ms, 20ms, 40ms)
- Applies to both SendLog and SendEvent methods
- Centralized in log client so all services benefit
- Should eliminate 'failed to send log' errors from concurrent requests

This fixes the issue where attestation-service logs weren't
appearing in CVMS output due to dropped messages.

* fix: Flatten sample evidence fields in primary_evidence for KBS

KBS Sample Verifier expects svn and report_data at the top level
of primary_evidence, not nested under an 'evidence' key.

Changed structure from:
{"primary_evidence": {"tee": "sample", "evidence": {"svn": "1", ...}}}

To:
{"primary_evidence": {"tee": "sample", "svn": "1", "report_data": "...", ...}}

This matches what KBS expects when deserializing the Quote structure.

* fix: Use sample quote directly as primary_evidence per KBS protocol

According to KBS attestation protocol spec, for sample TEE type,
primary_evidence should be the sample quote JSON directly:
{"svn": "1", "report_data": "..."}

Removed extra 'tee' and 'platform' fields that were causing KBS
to fail deserializing the Quote structure. The 'tee' field is
already sent in the Request payload during RCAR handshake.

Refs:
- https://github.com/confidential-containers/trustee/blob/main/kbs/docs/kbs_attestation_protocol.md
- https://github.com/confidential-containers/guest-components/blob/main/attestation-agent/attester/src/sample/mod.rs

* fix: Make CC AA required for sample attestation when configured

When USE_CC_ATTESTATION_AGENT=true, attestation-service now
requires AA to be available for NoCC/sample platform. This ensures
sample evidence always comes from AA with the correct KBS format.

Changes:
- Error out if AA connection fails for NoCC platform when AA is configured
- Only use EmptyProvider if AA is explicitly NOT configured
- Prevents incorrect sample evidence format from EmptyProvider

This ensures attestation-service delegates to AA for sample evidence
generation instead of creating it itself.

* fix: Implement proper RCAR protocol with tee-pubkey and runtime-data hash

Fixed KBS attestation error 'REPORT_DATA is different from that in Sample Quote'

Changes:
1. Generate ephemeral EC key pair BEFORE getting evidence from AA
2. Create runtime-data with nonce + tee-pubkey (JWK format)
3. Hash runtime-data (SHA-256) and use as report_data for AA
4. This binds the tee-pubkey to the TEE evidence per RCAR protocol

The report_data in the evidence now matches what KBS expects:
hash(runtime-data) instead of computation ID.

This completes the full RCAR protocol implementation:
- Request → Challenge → Attestation (with bound tee-pubkey) → Response

* fix(agent): use simple nonce for Sample attestation report_data

For Sample/NoCC attestation, use the raw nonce bytes directly as
report_data instead of hashing runtime-data. This avoids JSON
serialization mismatches with the KBS Sample verifier.

Real TEEs (TDX/SNP) still use runtime-data hash binding to
cryptographically bind the ephemeral tee-pubkey to the evidence.

* fix(agent): use RFC 8785 canonical JSON for runtime-data hashing

The KBS Sample attestation verifier (and likely others) expects the
report_data to be the SHA-256 hash of the *canonical* JSON serialization
(RFC 8785) of the runtime-data. Standard Go JSON marshaling does not
guarantee key ordering, leading to hash mismatches.

This change uses github.com/gowebpki/jcs to canonicalize the runtime-data
before hashing, ensuring compatibility with the KBS RCAR implementation.
Also reverted the temporary 'simple nonce' workaround.

* feat(hal): add CoCo Keyprovider and Skopeo packages

- Add coco-keyprovider buildroot package with systemd service
- Add skopeo buildroot package for OCI image handling
- Add ocicrypt_keyprovider.conf for encrypted image decryption
- Update Config.in to include new packages

This enables standard CoCo ecosystem integration for encrypted
OCI images instead of custom S3/HTTP registry clients.

* feat(oci): add OCI image handling package with Skopeo integration

- Add pkg/oci/types.go with ResourceSource and ImageManifest types
- Add pkg/oci/skopeo.go with Skopeo wrapper for pull/decrypt
- Add pkg/oci/extract.go for extracting algorithms and datasets from layers

This package provides OCI image handling using Skopeo and CoCo
Keyprovider for encrypted image decryption, replacing custom
S3/HTTP registry clients.

* chore: regenerate protobuf files for updated cvms.proto

* refactor(agent): replace S3/HTTP/KBS with OCI package

- Remove pkg/kbs and pkg/registry imports
- Add pkg/oci import for OCI image handling
- Replace downloadAndDecryptResource with OCI-based implementation
- Use Skopeo + CoCo Keyprovider for automatic decryption
- Reduce code from ~240 lines to ~70 lines

This eliminates custom KBS RCAR handshake, S3/HTTP registry clients,
and manual decryption logic. CoCo Keyprovider handles all decryption
automatically via ocicrypt protocol.

* chore: remove obsolete pkg/kbs and pkg/registry packages

- Delete pkg/kbs/ (custom KBS client, ~300 lines)
- Delete pkg/registry/ (S3/HTTP registry clients, ~400 lines)
- Remove unused imports from agent/service.go
- Run go mod tidy to clean up dependencies

These packages have been replaced by pkg/oci with Skopeo and
CoCo Keyprovider for standard CoCo ecosystem integration.

* fix(agent): update ResourceSource struct to include type and encryption fields

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

* fix(hal): update CoCo Keyprovider to v0.16.0 and fix build path

- Update version from v0.11.0 to v0.16.0 (matches attestation agent)
- Fix install path: target is at repo root, not in coco_keyprovider subdir
- This fixes the build error where coco_keyprovider binary wasn't found

The cargo workspace in guest-components builds to a shared target/
directory at the repository root, not within each crate's subdirectory.

* feat: Update remote resources testing guide to use kbs-client and coco-keyprovider for key management and encryption, enable insecure TLS for Skopeo, and enhance CVMS with

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

* feat: Update component versions, revise image encryption documentation, and sanitize OCI image paths for Skopeo compatibility.

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

* feat: Add `decompress` option to Dataset and `algo_type`/`algo_args` to Algorithm protobuf messages, updating client, test, and build configurations.

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

* Update multiple package versions and enhance OCI image extraction error reporting for missing algorithm files.

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

* chore: Bump package versions, improve OCI image extraction debugging by returning seen files, and remove unused dataset type parsing from test code.

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

* refactor: Migrate OCI extraction to use structured logging with `slog` and `context`, and update package versions.

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

* feat: Bump multiple component versions, add encrypted status for computation inputs and algorithms, and refine OCI layer extraction warnings.

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

* logging

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

* feat: Add `Encrypted` field to algorithm and dataset resource sources and update all component versions.

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

* feat: update component versions, integrate coco-keyprovider service, and configure ocicrypt key provider.

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

* feat: add support for KBS parameters and dataset/algorithm hash calculations in CVMS

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

* feat: update resource download and extraction logic to support requirements.txt and improve hash verification

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

* chore: Update dependencies, improve code style, and add GetRawEvidence to attestation client mocks.

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

* Refactor code structure for improved readability and maintainability

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

* fix: update golangci configuration to include errcheck for build path and remove unnecessary exclusions

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

* fix: streamline kernel command line handling in QEMU args construction

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

* feat: add attestation binary and update checksum tests and policy structure

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

* Add unit tests for attestation agent, attestation, log, crypto, OCI, and Skopeo clients

- Implement tests for the attestation agent client including Unix socket and TCP address handling, token retrieval, and error scenarios.
- Enhance attestation client tests to cover fetching raw evidence for various platforms (SNP, TDX, VTPM, SNPvTPM) and validate error handling.
- Introduce log client tests to verify retry behavior for sending logs and events.
- Create comprehensive tests for crypto package focusing on AES-GCM decryption, encrypted resource parsing, and key unwrapping.
- Add tests for OCI package to validate algorithm and dataset extraction, including JSON serialization of OCILayout.
- Implement Skopeo client tests to ensure proper functionality for image pulling, inspecting, and resource source handling.

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

* fix: handle JSON marshal errors in test cases for decrypt and extract functions

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

* test: add comprehensive tests for algorithm and dataset extraction with various scenarios

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

* refactor: replace hardcoded Python script content with constant variable

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

* fix: remove redundant mock expectation for SendAgentConfig in TestCreateVMWithAaKbsParams

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

* test: add tests for event sending failure, dataset extraction with path traversal, and Skopeo client behavior

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

* test: add tests for download and decryption of resources with various URL formats

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

* refactor: Introduce OCIClient interface for agent service to improve testability of OCI image operations and enhance related tests.

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

* refactor: Change `get_uint64_from_tcb` to accept `TcbVersion` by value and use `u64::from` for type conversions.

---------

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

* initial FDE setup

* add Manager support

* add cloud-init script

* rebase onto main

* add blank lines

* add tdx rtmr support

* add FDE flow

* use DiskConfig.Format instead of fixed values

* add tests and expand Manager README.md

* add curl command

* add encrypted partition support

* remove nbd

* add dm-verity

* fix manager boot sequence

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
Co-authored-by: ultraviolet <cocosai@worker-52.local.pragmatic-it.com>
Co-authored-by: Sammy Kerata Oina <44265300+SammyOina@users.noreply.github.com>
2026-05-08 14:59:13 +02:00
121 changed files with 6279 additions and 327 deletions
+3 -1
View File
@@ -19,6 +19,7 @@ target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
!tools/nvidia-attestation-helper/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
@@ -28,4 +29,5 @@ Cargo.lock
*.enc
*.key
*.pub
*.pub
.codex
+50 -5
View File
@@ -1,11 +1,24 @@
BUILD_DIR = build
SERVICES = manager agent cli attestation-service log-forwarder computation-runner egress-proxy ingress-proxy
NVIDIA_ATTESTATION_HELPER = nvidia-attestation-helper
NVIDIA_ATTESTATION_HELPER_DIR = tools/$(NVIDIA_ATTESTATION_HELPER)
NVIDIA_ATTESTATION_HELPER_MANIFEST = $(NVIDIA_ATTESTATION_HELPER_DIR)/Cargo.toml
NVIDIA_ATTESTATION_HELPER_BINARY = $(BUILD_DIR)/$(NVIDIA_ATTESTATION_HELPER)
NVIDIA_ATTESTATION_HELPER_LIB_DIR = $(BUILD_DIR)/lib
NVAT_SDK_CPP_DIR ?= $(firstword $(wildcard $(HOME)/.cargo/git/checkouts/attestation-sdk-*/*/nv-attestation-sdk-cpp))
NVAT_SDK_CPP_BUILD_DIR ?= $(NVAT_SDK_CPP_DIR)/build
NVAT_SDK_HEADER ?= $(NVAT_SDK_CPP_BUILD_DIR)/include/nvat.h
NVAT_SDK_SHARED_LIB ?= $(NVAT_SDK_CPP_BUILD_DIR)/libnvat.so.1
NVAT_SYSTEM_HEADER ?= /usr/include/nvat.h
CARGO ?= cargo
CMAKE ?= cmake
CGO_ENABLED ?= 0
GOARCH ?= amd64
VERSION ?= $(shell git describe --abbrev=0 --tags --always)
COMMIT ?= $(shell git rev-parse HEAD)
TIME ?= $(shell date +%F_%T)
EMBED_ENABLED ?= 0
NVAT_USE_SYSTEM_LIB ?=
INSTALL_DIR ?= /usr/local/bin
CONFIG_DIR ?= /etc/cocos
SERVICE_NAME ?= cocos-manager
@@ -16,21 +29,53 @@ IGVM_BUILD_SCRIPT := ./scripts/igvmmeasure/igvm.sh
define compile_service
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
go build -ldflags "-s -w \
-X 'github.com/absmach/supermq.BuildTime=$(TIME)' \
-X 'github.com/absmach/supermq.Version=$(VERSION)' \
-X 'github.com/absmach/supermq.Commit=$(COMMIT)'" \
-X 'github.com/absmach/magistrala.BuildTime=$(TIME)' \
-X 'github.com/absmach/magistrala.Version=$(VERSION)' \
-X 'github.com/absmach/magistrala.Commit=$(COMMIT)'" \
$(if $(filter 1,$(EMBED_ENABLED)),-tags "embed",) \
-o ${BUILD_DIR}/cocos-$(1) ./cmd/$(1)
endef
.PHONY: all $(SERVICES) install clean
NVIDIA_ATTESTATION_HELPER_CARGO_ENV = $(if $(filter 1,$(NVAT_USE_SYSTEM_LIB)),NVAT_USE_SYSTEM_LIB=1,)
NVIDIA_ATTESTATION_HELPER_RUSTFLAGS = $(strip $(RUSTFLAGS) $(if $(filter 1,$(NVAT_USE_SYSTEM_LIB)),,-C link-arg=-Wl,-rpath,$$ORIGIN/lib))
.PHONY: all $(SERVICES) $(NVIDIA_ATTESTATION_HELPER) nvidia-attestation-helper-prereqs install clean
all: $(SERVICES)
$(SERVICES):
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(SERVICES): | $(BUILD_DIR)
$(call compile_service,$@)
@if [ "$@" = "cli" ] || [ "$@" = "manager" ]; then $(MAKE) build-igvm; fi
nvidia-attestation-helper-prereqs:
ifeq ($(filter 1,$(NVAT_USE_SYSTEM_LIB)),1)
@test -f $(NVAT_SYSTEM_HEADER) || \
( echo "Missing $(NVAT_SYSTEM_HEADER). Install the NVAT development package or run without NVAT_USE_SYSTEM_LIB=1."; exit 1 )
@ldconfig -p | grep -q libnvat.so.1 || \
( echo "libnvat.so.1 not found in the dynamic linker cache. Install the NVAT runtime package or run without NVAT_USE_SYSTEM_LIB=1."; exit 1 )
else
@if [ -z "$(NVAT_SDK_CPP_DIR)" ]; then \
echo "Unable to locate nv-attestation-sdk-cpp under $$HOME/.cargo/git/checkouts."; \
echo "Run 'cargo fetch --manifest-path $(NVIDIA_ATTESTATION_HELPER_MANIFEST)' first, or install NVAT and use 'make NVAT_USE_SYSTEM_LIB=1 $(NVIDIA_ATTESTATION_HELPER)'."; \
exit 1; \
fi
@if [ ! -f "$(NVAT_SDK_HEADER)" ] || [ ! -f "$(NVAT_SDK_SHARED_LIB)" ]; then \
$(CMAKE) -S $(NVAT_SDK_CPP_DIR) -B $(NVAT_SDK_CPP_BUILD_DIR) && \
$(CMAKE) --build $(NVAT_SDK_CPP_BUILD_DIR); \
fi
endif
$(NVIDIA_ATTESTATION_HELPER): nvidia-attestation-helper-prereqs | $(BUILD_DIR)
RUSTFLAGS='$(NVIDIA_ATTESTATION_HELPER_RUSTFLAGS)' $(NVIDIA_ATTESTATION_HELPER_CARGO_ENV) $(CARGO) build --manifest-path $(NVIDIA_ATTESTATION_HELPER_MANIFEST) --release
install -m 755 $(NVIDIA_ATTESTATION_HELPER_DIR)/target/release/$(NVIDIA_ATTESTATION_HELPER) $(NVIDIA_ATTESTATION_HELPER_BINARY)
@if [ "$(filter 1,$(NVAT_USE_SYSTEM_LIB))" != "1" ]; then \
install -d $(NVIDIA_ATTESTATION_HELPER_LIB_DIR); \
install -m 755 $(NVAT_SDK_SHARED_LIB) $(NVIDIA_ATTESTATION_HELPER_LIB_DIR)/libnvat.so.1; \
fi
protoc:
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative agent/agent.proto
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative manager/manager.proto
+8
View File
@@ -21,12 +21,20 @@ The service is configured using the environment variables from the following tab
| AGENT_CVM_ID | Unique identifier for the CVM (Confidential Virtual Machine) | "" |
| AGENT_CERTS_TOKEN | Authentication token for certificate service access | "" |
| AGENT_MAA_URL | Microsoft Azure Attestation service URL for Azure attestation | https://sharedeus2.eus2.attest.azure.net |
| AZURE_TDX_IMDS_URL | Azure TDX quote endpoint used by direct Azure TDX attestation | http://169.254.169.254/acc/tdquote |
| AZURE_HCL_REFRESH_WAIT | Wait after writing TDX report data to Azure HCL vTPM storage before reading the refreshed HCL report | 3s |
| AGENT_OS_BUILD | Operating system build information for attestation | UVC |
| AGENT_OS_DISTRO | Operating system distribution information for attestation | UVC |
| AGENT_OS_TYPE | Operating system type information for attestation | UVC |
| ATTESTATION_SERVICE_SOCKET | Unix socket path for attestation service communication | /run/cocos/attestation.sock |
| AGENT_ENABLE_ATLS | Enable Attestation TLS for secure communication | true |
### Azure TDX Attestation
When the agent runs on an Azure TDX CVM, Azure attestation uses the direct Azure TDX flow. The agent writes TDX report data to Azure HCL vTPM storage, reads the refreshed HCL report, requests a TD quote from Azure IMDS, and submits the quote plus HCL runtime data to Microsoft Azure Attestation. This path does not depend on Confidential Containers attestation-agent `GetEvidence` or KBS token retrieval.
`AGENT_MAA_URL` selects the Microsoft Azure Attestation endpoint. `AZURE_TDX_IMDS_URL` can override the Azure IMDS TDX quote endpoint, and `AZURE_HCL_REFRESH_WAIT` controls the wait used to avoid reading a stale HCL report after report-data is written.
### Remote Resource Download (Optional)
The agent supports downloading encrypted algorithms and datasets from remote registries (S3, HTTP/HTTPS) and retrieving decryption keys from a Key Broker Service (KBS) via attestation.
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"strings"
"testing"
mglog "github.com/absmach/supermq/logger"
mglog "github.com/absmach/magistrala/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/ultravioletrs/cocos/agent/events/mocks"
+1 -1
View File
@@ -60,7 +60,7 @@ func (req azureAttestationTokenReq) validate() error {
func validateAttestationType(attType attestation.PlatformType) error {
switch attType {
case attestation.SNP, attestation.VTPM, attestation.SNPvTPM, attestation.TDX:
case attestation.SNP, attestation.VTPM, attestation.SNPvTPM, attestation.Azure, attestation.TDX:
return nil
default:
return errors.New("invalid attestation type")
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"crypto/x509"
"encoding/base64"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/ultravioletrs/cocos/agent"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"encoding/base64"
"testing"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent"
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/agent/cvms"
"github.com/ultravioletrs/cocos/agent/cvms/api/grpc/storage"
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
mglog "github.com/absmach/supermq/logger"
mglog "github.com/absmach/magistrala/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/ultravioletrs/cocos/agent"
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/ultravioletrs/cocos/agent/cvms"
+28 -5
View File
@@ -19,7 +19,7 @@ import (
sync "sync"
"time"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/ultravioletrs/cocos/agent/algorithm"
"github.com/ultravioletrs/cocos/agent/events"
runnerpb "github.com/ultravioletrs/cocos/agent/runner"
@@ -85,6 +85,29 @@ var (
ImaPcrIndex = 10
)
func ensureDir(path string, mode os.FileMode) error {
info, err := os.Stat(path)
switch {
case err == nil:
if info.IsDir() {
return nil
}
if err := os.Remove(path); err != nil {
return fmt.Errorf("removing non-directory path %q: %w", path, err)
}
case os.IsNotExist(err):
// Continue and create it below.
default:
return fmt.Errorf("stating path %q: %w", path, err)
}
if err := os.MkdirAll(path, mode); err != nil {
return fmt.Errorf("creating directory %q: %w", path, err)
}
return nil
}
var (
// ErrMalformedEntity indicates malformed entity specification (e.g.
// invalid username or password).
@@ -478,8 +501,8 @@ func (as *agentService) downloadAlgorithmIfRemote(state statemachine.State) {
as.algoReceived = true
as.algoRequirements = res.Requirements // Store requirements for installation
// Create datasets directory
if err := os.Mkdir(algorithm.DatasetsDir, 0o755); err != nil {
// The initramfs may have already provisioned /cocos/datasets.
if err := ensureDir(algorithm.DatasetsDir, 0o755); err != nil {
as.runError = fmt.Errorf("error creating datasets directory: %w", err)
as.logger.Error(as.runError.Error())
as.sm.SendEvent(RunFailed)
@@ -976,7 +999,7 @@ func (as *agentService) Algo(ctx context.Context, algo Algorithm) error {
as.algoRequirements = algo.Requirements
as.algoReceived = true
if err := os.Mkdir(algorithm.DatasetsDir, 0o755); err != nil {
if err := ensureDir(algorithm.DatasetsDir, 0o755); err != nil {
return fmt.Errorf("error creating datasets directory: %v", err)
}
@@ -1145,7 +1168,7 @@ func (as *agentService) runComputation(state statemachine.State) {
}
}()
if err := os.Mkdir(algorithm.ResultsDir, 0o755); err != nil {
if err := ensureDir(algorithm.ResultsDir, 0o755); err != nil {
as.mu.Lock()
as.runError = fmt.Errorf("error creating results directory: %s", err.Error())
as.mu.Unlock()
+2 -2
View File
@@ -18,8 +18,8 @@ import (
"testing"
"time"
mglog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/errors"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os"
"strings"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/google/go-sev-guest/abi"
tpmAttest "github.com/google/go-tpm-tools/proto/attest"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os"
"testing"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"os"
"path"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/agent"
+1 -1
View File
@@ -3,7 +3,7 @@
package cli
import (
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/agent/auth"
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"errors"
"testing"
mgerrors "github.com/absmach/supermq/pkg/errors"
mgerrors "github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/agent/auth"
+11 -5
View File
@@ -17,8 +17,8 @@ import (
"syscall"
"github.com/absmach/certs/sdk"
mglog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/prometheus"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/prometheus"
"github.com/caarlos0/env/v11"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/agent/api"
@@ -269,7 +269,7 @@ func main() {
return mc.Process(ctx, cancel)
})
attest, certSerialNumber, err := attestationFromCert(ctx, cvmGrpcConfig.ClientCert, svc)
attest, certSerialNumber, err := attestationFromCert(ctx, cvmGrpcConfig.ClientCert, svc, ccPlatform)
if err != nil {
logger.Error(fmt.Sprintf("failed to get attestation: %s", err))
exitCode = 1
@@ -317,7 +317,7 @@ func newService(ctx context.Context, logger *slog.Logger, eventSvc events.Servic
return svc
}
func attestationFromCert(ctx context.Context, certFilePath string, svc agent.Service) ([]byte, string, error) {
func attestationFromCert(ctx context.Context, certFilePath string, svc agent.Service, ccPlatform attestation.PlatformType) ([]byte, string, error) {
if certFilePath == "" {
return nil, "", nil
}
@@ -328,6 +328,9 @@ func attestationFromCert(ctx context.Context, certFilePath string, svc agent.Ser
}
certPem, _ := pem.Decode(certFile)
if certPem == nil {
return nil, "", fmt.Errorf("failed to decode certificate PEM")
}
certx509, err := x509.ParseCertificate(certPem.Bytes)
if err != nil {
return nil, "", err
@@ -335,7 +338,7 @@ func attestationFromCert(ctx context.Context, certFilePath string, svc agent.Ser
nonceSNP := sha512.Sum512(certFile)
nonceVTPM := sha256.Sum256(certFile)
attest, err := svc.Attestation(ctx, nonceSNP, nonceVTPM, attestation.SNPvTPM)
attest, err := svc.Attestation(ctx, nonceSNP, nonceVTPM, ccPlatform)
if err != nil {
return nil, "", err
}
@@ -354,6 +357,9 @@ func azureAttestationFromCert(ctx context.Context, certFilePath string, svc agen
}
certPem, _ := pem.Decode(certFile)
if certPem == nil {
return nil, "", fmt.Errorf("failed to decode certificate PEM")
}
certx509, err := x509.ParseCertificate(certPem.Bytes)
if err != nil {
return nil, "", err
@@ -33,6 +33,12 @@ func (s *service) FetchRawEvidence(ctx context.Context, req *attestationpb.Attes
var nonce [32]byte
copy(nonce[:], req.Nonce)
binaryReport, err = s.provider.Attestation(reportData[:], nonce[:])
case attestationpb.PlatformType_PLATFORM_TYPE_AZURE:
var reportData [64]byte
copy(reportData[:], req.ReportData)
var nonce [32]byte
copy(nonce[:], req.Nonce)
binaryReport, err = s.provider.Attestation(reportData[:], nonce[:])
case attestationpb.PlatformType_PLATFORM_TYPE_UNSPECIFIED:
// Generate sample attestation for testing in non-TEE environments
// This uses the underlying provider (EmptyProvider or CC Attestation Agent)
+76
View File
@@ -0,0 +1,76 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"crypto/sha256"
"fmt"
"strings"
attestationpb "github.com/ultravioletrs/cocos/internal/proto/attestation/v1"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
)
func newGPUCollector(cfg config) (attestationgpu.Collector, error) {
if strings.TrimSpace(cfg.GPUHelperPath) == "" {
return nil, nil
}
return attestationgpu.NewCommandCollector(cfg.GPUHelperPath, cfg.GPUHelperTimeout)
}
func (s *service) claimOptions(ctx context.Context, req *attestationpb.AttestationRequest, platformType attestation.PlatformType) ([]eat.ClaimsOption, error) {
var opts []eat.ClaimsOption
if s.gpuCollector != nil && shouldCollectGPU(platformType) {
sessionNonce := requestNonce(req)
gpuNonce := deriveComponentNonce(sessionNonce, "gpu")
evidence, err := s.gpuCollector.Collect(ctx, gpuNonce)
if err != nil {
// GPU evidence is opportunistic: if no supported CC-capable GPU is
// attached, or the helper cannot collect evidence, we continue with
// the root CPU/TEE attestation instead of failing the whole request.
s.logger.Warn(fmt.Sprintf("[ATTESTATION-SERVICE] Skipping optional GPU evidence collection: %s", err))
return opts, nil
}
s.logger.Info(fmt.Sprintf("[ATTESTATION-SERVICE] Collected GPU evidence: format=%s bytes=%d",
evidence.EvidenceFormat, len(evidence.RawEvidence)))
opts = append(opts, eat.WithGPU(&eat.GPUExtensions{
Vendor: evidence.Vendor,
EvidenceFormat: evidence.EvidenceFormat,
Nonce: gpuNonce,
EvidenceJSON: evidence.RawEvidence,
}))
}
return opts, nil
}
func shouldCollectGPU(platformType attestation.PlatformType) bool {
switch platformType {
case attestation.SNP, attestation.SNPvTPM, attestation.TDX, attestation.Azure:
return true
default:
return false
}
}
func requestNonce(req *attestationpb.AttestationRequest) []byte {
if len(req.Nonce) > 0 {
return append([]byte(nil), req.Nonce...)
}
return append([]byte(nil), req.ReportData...)
}
func deriveComponentNonce(sessionNonce []byte, component string) []byte {
digest := sha256.Sum256(append(append([]byte(nil), sessionNonce...), []byte(":"+component)...))
return digest[:]
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"io"
"log/slog"
"testing"
"github.com/stretchr/testify/assert"
attestationpb "github.com/ultravioletrs/cocos/internal/proto/attestation/v1"
"github.com/ultravioletrs/cocos/pkg/attestation"
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
)
func TestRequestNonce(t *testing.T) {
req := &attestationpb.AttestationRequest{
ReportData: []byte("report"),
Nonce: []byte("nonce"),
}
assert.Equal(t, []byte("nonce"), requestNonce(req))
req.Nonce = nil
assert.Equal(t, []byte("report"), requestNonce(req))
}
func TestDeriveComponentNonce(t *testing.T) {
sessionNonce := []byte("session-nonce")
gpuNonce := deriveComponentNonce(sessionNonce, "gpu")
gpuNonceAgain := deriveComponentNonce(sessionNonce, "gpu")
teeNonce := deriveComponentNonce(sessionNonce, "tee")
assert.Len(t, gpuNonce, 32)
assert.Equal(t, gpuNonce, gpuNonceAgain)
assert.NotEqual(t, gpuNonce, teeNonce)
}
func TestShouldCollectGPU(t *testing.T) {
assert.True(t, shouldCollectGPU(attestation.SNP))
assert.True(t, shouldCollectGPU(attestation.SNPvTPM))
assert.True(t, shouldCollectGPU(attestation.TDX))
assert.False(t, shouldCollectGPU(attestation.VTPM))
assert.False(t, shouldCollectGPU(attestation.NoCC))
}
func TestNewGPUCollector(t *testing.T) {
collector, err := newGPUCollector(config{})
assert.NoError(t, err)
assert.Nil(t, collector)
collector, err = newGPUCollector(config{
GPUHelperPath: "/tmp/helper",
GPUHelperTimeout: 0,
})
assert.NoError(t, err)
assert.NotNil(t, collector)
}
func TestClaimOptions_SkipsOptionalGPUFailure(t *testing.T) {
svc := &service{
logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
gpuCollector: failingCollector{},
}
req := &attestationpb.AttestationRequest{
ReportData: []byte("report-data"),
Nonce: []byte("nonce-data"),
}
opts, err := svc.claimOptions(context.Background(), req, attestation.TDX)
assert.NoError(t, err)
assert.Empty(t, opts)
}
type failingCollector struct{}
func (failingCollector) Collect(context.Context, []byte) (*attestationgpu.Evidence, error) {
return nil, assert.AnError
}
+64 -26
View File
@@ -11,8 +11,9 @@ import (
"os"
"os/signal"
"syscall"
"time"
mglog "github.com/absmach/supermq/logger"
mglog "github.com/absmach/magistrala/logger"
"github.com/caarlos0/env/v11"
"github.com/ultravioletrs/cocos/agent/cvms"
logpb "github.com/ultravioletrs/cocos/agent/log"
@@ -22,6 +23,7 @@ import (
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/ccaa"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
attestationgpu "github.com/ultravioletrs/cocos/pkg/attestation/gpu"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
logclient "github.com/ultravioletrs/cocos/pkg/clients/grpc/log"
@@ -35,16 +37,29 @@ const (
)
type config struct {
LogLevel string `env:"ATTESTATION_LOG_LEVEL" envDefault:"debug"`
Vmpl int `env:"ATTESTATION_VMPL" envDefault:"2"`
AgentMaaURL string `env:"AGENT_MAA_URL" envDefault:"https://sharedeus2.eus2.attest.azure.net"`
AgentOSBuild string `env:"AGENT_OS_BUILD" envDefault:"UVC"`
AgentOSDistro string `env:"AGENT_OS_DISTRO" envDefault:"UVC"`
AgentOSType string `env:"AGENT_OS_TYPE" envDefault:"UVC"`
EATFormat string `env:"ATTESTATION_EAT_FORMAT" envDefault:"CBOR"` // JWT or CBOR
EATIssuer string `env:"ATTESTATION_EAT_ISSUER" envDefault:"cocos-attestation-service"`
UseCCAttestationAgent bool `env:"USE_CC_ATTESTATION_AGENT" envDefault:"false"`
CCAgentAddress string `env:"CC_AGENT_ADDRESS" envDefault:"127.0.0.1:50002"`
LogLevel string `env:"ATTESTATION_LOG_LEVEL" envDefault:"debug"`
Vmpl int `env:"ATTESTATION_VMPL" envDefault:"2"`
AgentMaaURL string `env:"AGENT_MAA_URL" envDefault:"https://sharedeus2.eus2.attest.azure.net"`
AgentOSBuild string `env:"AGENT_OS_BUILD" envDefault:"UVC"`
AgentOSDistro string `env:"AGENT_OS_DISTRO" envDefault:"UVC"`
AgentOSType string `env:"AGENT_OS_TYPE" envDefault:"UVC"`
EATFormat string `env:"ATTESTATION_EAT_FORMAT" envDefault:"CBOR"` // JWT or CBOR
EATIssuer string `env:"ATTESTATION_EAT_ISSUER" envDefault:"cocos-attestation-service"`
UseCCAttestationAgent bool `env:"USE_CC_ATTESTATION_AGENT" envDefault:"false"`
CCAgentAddress string `env:"CC_AGENT_ADDRESS" envDefault:"127.0.0.1:50002"`
GPUHelperPath string `env:"ATTESTATION_GPU_HELPER_PATH" envDefault:""`
GPUHelperTimeout time.Duration `env:"ATTESTATION_GPU_HELPER_TIMEOUT" envDefault:"30s"`
// Future KBS Integration Configuration
// When KBS support is added, these fields will enable:
// - Remote attestation verification via KBS
// - Encrypted algorithm/dataset retrieval
// - Per-computation secret provisioning
//
// Example future fields:
// KBSEndpoint string `env:"KBS_ENDPOINT" envDefault:""` // Optional KBS URL
// KBSEnabled bool `env:"KBS_ENABLED" envDefault:"false"`
// KBSTimeout int `env:"KBS_TIMEOUT_SECONDS" envDefault:"30"`
}
func main() {
@@ -141,8 +156,10 @@ func main() {
}
}
isDirectProvider := false
// Fallback to direct providers if CC AA not configured or unavailable
if provider == nil {
isDirectProvider = true
switch ccPlatform {
case attestation.SNP:
provider = vtpm.NewProvider(false, uint(cfg.Vmpl))
@@ -173,7 +190,7 @@ func main() {
logger.Error("[ATTESTATION-SERVICE] No provider configured!")
}
if ccPlatform == attestation.SNP || ccPlatform == attestation.SNPvTPM {
if (ccPlatform == attestation.SNP || ccPlatform == attestation.SNPvTPM) && isDirectProvider {
if err := vtpm.FetchSEVCertificates(uint(cfg.Vmpl)); err != nil {
logger.Error(fmt.Sprintf("failed to fetch certificates: %s", err))
exitCode = 1
@@ -218,13 +235,24 @@ func main() {
return
}
gpuCollector, err := newGPUCollector(cfg)
if err != nil {
logger.Error(fmt.Sprintf("failed to configure GPU attestation collector: %s", err))
exitCode = 1
return
}
if gpuCollector != nil {
logger.Info(fmt.Sprintf("[ATTESTATION-SERVICE] GPU evidence collection enabled via helper %s", cfg.GPUHelperPath))
}
grpcServer := grpc.NewServer()
svc := &service{
provider: provider,
logger: logger,
signingKey: signingKey,
eatFormat: cfg.EATFormat,
eatIssuer: cfg.EATIssuer,
provider: provider,
logger: logger,
signingKey: signingKey,
eatFormat: cfg.EATFormat,
eatIssuer: cfg.EATIssuer,
gpuCollector: gpuCollector,
}
attestationpb.RegisterAttestationServiceServer(grpcServer, svc)
@@ -256,11 +284,12 @@ func main() {
type service struct {
attestationpb.UnimplementedAttestationServiceServer
provider attestation.Provider
logger *slog.Logger
signingKey *ecdsa.PrivateKey
eatFormat string
eatIssuer string
provider attestation.Provider
logger *slog.Logger
signingKey *ecdsa.PrivateKey
eatFormat string
eatIssuer string
gpuCollector attestationgpu.Collector
}
func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.AttestationRequest) (*attestationpb.AttestationResponse, error) {
@@ -291,6 +320,13 @@ func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.Attes
copy(nonce[:], req.Nonce)
binaryReport, err = s.provider.Attestation(reportData[:], nonce[:])
platformType = attestation.SNPvTPM
case attestationpb.PlatformType_PLATFORM_TYPE_AZURE:
var reportData [64]byte
copy(reportData[:], req.ReportData)
var nonce [32]byte
copy(nonce[:], req.Nonce)
binaryReport, err = s.provider.Attestation(reportData[:], nonce[:])
platformType = attestation.Azure
case attestationpb.PlatformType_PLATFORM_TYPE_UNSPECIFIED:
// Generate sample attestation for testing in non-TEE environments
s.logger.Warn("generating sample attestation for PLATFORM_TYPE_UNSPECIFIED - this should only be used for testing")
@@ -319,12 +355,14 @@ func (s *service) FetchAttestation(ctx context.Context, req *attestationpb.Attes
}
// Create EAT claims from binary report
nonce := req.ReportData
if len(req.Nonce) > 0 {
nonce = req.Nonce
nonce := requestNonce(req)
claimOpts, err := s.claimOptions(ctx, req, platformType)
if err != nil {
return nil, err
}
claims, err := eat.NewEATClaims(binaryReport, nonce, platformType)
claims, err := eat.NewEATClaims(binaryReport, nonce, platformType, claimOpts...)
if err != nil {
s.logger.Error(fmt.Sprintf("failed to create EAT claims: %s", err))
return nil, fmt.Errorf("failed to create EAT claims: %w", err)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"os/signal"
"syscall"
mglog "github.com/absmach/supermq/logger"
mglog "github.com/absmach/magistrala/logger"
"github.com/caarlos0/env/v11"
"github.com/ultravioletrs/cocos/agent/cvms"
logpb "github.com/ultravioletrs/cocos/agent/log"
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"os/signal"
"syscall"
mglog "github.com/absmach/supermq/logger"
mglog "github.com/absmach/magistrala/logger"
"github.com/caarlos0/env/v11"
"github.com/ultravioletrs/cocos/agent/cvms"
pb "github.com/ultravioletrs/cocos/agent/log"
+7 -7
View File
@@ -12,13 +12,13 @@ import (
"os"
"strings"
mglog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/jaeger"
"github.com/absmach/supermq/pkg/prometheus"
smqserver "github.com/absmach/supermq/pkg/server"
grpcserver "github.com/absmach/supermq/pkg/server/grpc"
httpserver "github.com/absmach/supermq/pkg/server/http"
"github.com/absmach/supermq/pkg/uuid"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/jaeger"
"github.com/absmach/magistrala/pkg/prometheus"
smqserver "github.com/absmach/magistrala/pkg/server"
grpcserver "github.com/absmach/magistrala/pkg/server/grpc"
httpserver "github.com/absmach/magistrala/pkg/server/http"
"github.com/absmach/magistrala/pkg/uuid"
"github.com/caarlos0/env/v11"
"github.com/go-chi/chi/v5"
"github.com/ultravioletrs/cocos/manager"
+47 -46
View File
@@ -4,48 +4,51 @@ go 1.26.0
require (
github.com/caarlos0/env/v11 v11.4.0
github.com/fatih/color v1.18.0
github.com/fatih/color v1.19.0
github.com/go-kit/kit v0.13.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/google/go-sev-guest v0.14.1
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843
github.com/google/go-tdx-guest v0.3.2-0.20260605221019-34f07ec666c4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/virtee/sev-snp-measure-go v0.0.0-20240530153610-e6e8dc9b6877
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0
go.opentelemetry.io/otel/trace v1.41.0
golang.org/x/crypto v0.48.0
golang.org/x/sync v0.19.0
google.golang.org/grpc v1.79.1
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/crypto v0.50.0
golang.org/x/sync v0.20.0
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
)
require (
cloud.google.com/go/storage v1.57.2
github.com/absmach/supermq v0.19.0
cloud.google.com/go/storage v1.62.3
github.com/absmach/magistrala v0.20.0
github.com/caarlos0/env/v10 v10.0.0
github.com/fxamacker/cbor/v2 v2.9.0
github.com/fxamacker/cbor/v2 v2.9.2
github.com/go-chi/chi/v5 v5.2.5
github.com/go-jose/go-jose/v4 v4.1.3
github.com/go-jose/go-jose/v4 v4.1.4
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/gce-tcb-verifier v0.3.1
github.com/veraison/corim v1.1.2
github.com/veraison/go-cose v1.3.0
github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca
google.golang.org/api v0.274.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.19.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
cloud.google.com/go/iam v1.7.0 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
@@ -53,7 +56,7 @@ require (
github.com/containerd/log v0.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
@@ -65,8 +68,8 @@ require (
github.com/google/go-attestation v0.5.1 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
@@ -80,33 +83,31 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/veraison/eat v0.0.0-20210331113810-3da8a4dd42ff // indirect
github.com/veraison/swid v1.1.1-0.20230911094910-8ffdd07a22ca // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.247.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
gotest.tools/v3 v3.5.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)
require (
github.com/absmach/certs v0.18.2
github.com/absmach/certs v0.18.5
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -117,11 +118,11 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc // indirect
github.com/google/go-tpm v0.9.6
github.com/google/go-tpm-tools v0.4.7
github.com/google/go-tpm v0.9.8
github.com/google/go-tpm-tools v0.4.4
github.com/google/logger v1.1.1 // indirect
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -130,15 +131,15 @@ require (
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/stretchr/objx v0.5.3 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.49.0
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+112 -112
View File
@@ -1,43 +1,45 @@
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ=
cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/storage v1.57.2 h1:sVlym3cHGYhrp6XZKkKb+92I1V42ks2qKKpB0CF5Mb4=
cloud.google.com/go/storage v1.57.2/go.mod h1:n5ijg4yiRXXpCu0sJTD6k+eMf7GRrJmPyr9YxLXGHOk=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.62.3 h1:SZq1t23NCI+e96dH77Dg3PEfsNNEjqO8zE5AnD8gVD0=
cloud.google.com/go/storage v1.62.3/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 h1:owcC2UnmsZycprQ5RfRgjydWhuoxg71LUfyiQdijZuM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0 h1:4LP6hvB4I5ouTbGgWtixJhgED6xdf67twf9PoY96Tbg=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.53.0/go.mod h1:jUZ5LYlw40WMd07qxcQJD5M40aUxrfwqQX1g7zxYnrQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 h1:Ron4zCA/yk6U7WOBXhTJcDpsUBG9npumK6xw2auFltQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/absmach/certs v0.18.2 h1:s6KKL3/KfDZ6z0IxvNCksIOUwRnEgQyCpeAonuR15No=
github.com/absmach/certs v0.18.2/go.mod h1:scqVZsmW2xPScnpMTtE70oN6cn0LLjFcJVPi4JKZ4+E=
github.com/absmach/supermq v0.19.0 h1:sbqfzmSiMp9GEaCWgpREiLC0tFsSntgLIyAaZs7SnRY=
github.com/absmach/supermq v0.19.0/go.mod h1:SG2yIzlJmc26ZjDVSkoapc6HZ6W13SUsaN3sAErfgC4=
github.com/absmach/certs v0.18.5 h1:eYlvitou+LoDtt7ETVLTp6d/1xCejGL3EmVOg+rHGTU=
github.com/absmach/certs v0.18.5/go.mod h1:31dtVe1VYF16W+IvjAE/uPAIz4f3uLHgh+moBezjqIc=
github.com/absmach/magistrala v0.20.0 h1:3AQ0C2AMoOCc1UuJLhPNJLMrNRLZoN0ibSOERqEkM98=
github.com/absmach/magistrala v0.20.0/go.mod h1:lnuO4fSngMiRYyNYL4yz5UP8DX3bbXRm87b2KHFGwJU=
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4 h1:533pRc6R7perWDqJuZq+ofBQfYfmyj7n49V4LFY4zpo=
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4/go.mod h1:xDAX/O3VcOsHWCx2fk85VD7FI17hAUOvoOhho7DA7g0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
@@ -58,7 +60,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
@@ -67,8 +68,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/edgelesssys/go-azguestattestation v0.0.0-20250408071817-8c4457b235ff h1:V6A5kD0+c1Qg4X72Lg+zxhCZk+par436sQdgLvMCBBc=
@@ -81,22 +82,24 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
@@ -130,16 +133,14 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU=
github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo=
github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba h1:05m5+kgZjxYUZrx3bZfkKHl6wkch+Khao6N21rFHInk=
github.com/google/go-eventlog v0.0.2-0.20241003021507-01bb555f7cba/go.mod h1:7huE5P8w2NTObSwSJjboHmB7ioBNblkijdzoVa2skfQ=
github.com/google/go-sev-guest v0.14.1 h1:j/DXy9jk1qSW/dEV9vDiQnhAVFD1zqnWNVu6p1J0Jgo=
github.com/google/go-sev-guest v0.14.1/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760=
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A=
github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g=
github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA=
github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
github.com/google/go-tdx-guest v0.3.2-0.20260605221019-34f07ec666c4 h1:OX2Mksz5ZHxawvZskqYX18Xy/q292EoiyUAT4WXg/gU=
github.com/google/go-tdx-guest v0.3.2-0.20260605221019-34f07ec666c4/go.mod h1:uHy3VaNXNXhl0fiPxKqTxieeouqQmW6A0EfLcaeCYBk=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ=
@@ -151,30 +152,30 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -203,16 +204,16 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 h1:xzZOeCMQLA/W198ZkdVdt4EKFKJtS26B773zNU377ZY=
@@ -243,8 +244,8 @@ github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTC
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -255,7 +256,6 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -276,28 +276,28 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -308,8 +308,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -317,15 +317,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -336,42 +336,42 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.247.0 h1:tSd/e0QrUlLsrwMKmkbQhYVa109qIintOls2Wh6bngc=
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.274.0 h1:aYhycS5QQCwxHLwfEHRRLf9yNsfvp1JadKKWBE54RFA=
google.golang.org/api v0.274.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+9
View File
@@ -0,0 +1,9 @@
source "$BR2_EXTERNAL_COCOS_PATH/package/agent/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/attestation-service/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/cc-attestation-agent/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/coco-keyprovider/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/wasmedge/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/log-forwarder/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/computation-runner/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/egress-proxy/Config.in"
source "$BR2_EXTERNAL_COCOS_PATH/package/ingress-proxy/Config.in"
+214
View File
@@ -0,0 +1,214 @@
# Disk Image Workflow
This directory is the Buildroot external tree for the current Cocos disk test
VM image and its runtime configuration.
## Layout
- [configs/cocos_defconfig](./configs/cocos_defconfig):
Buildroot configuration for the bootable image.
- [board/rootfs-overlay/init](./board/rootfs-overlay/init):
early initramfs script that provisions `/cocos`, mounts the real root, and
switches into the installed system.
- [board/cocos/genimage.cfg](./board/cocos/genimage.cfg):
GPT disk layout for the final `disk.img`.
- [board/cocos/post-image.sh](./board/cocos/post-image.sh):
builds the minimal initramfs, stages EFI files, signs boot artifacts, and
assembles `disk.img`.
- [external.desc](./external.desc): Buildroot external tree descriptor.
- [external.mk](./external.mk): includes package makefiles from `package/*`.
## Current Buildroot Image
The current Buildroot flow produces a bootable GPT disk image:
- `efi` partition: FAT EFI system partition with GRUB, kernel, and initramfs
- `root` partition: ext4 root filesystem protected by dm-verity
- `verity` partition: dm-verity hash tree for the root filesystem
- `cocos` partition: blank partition provisioned at boot as an encrypted ext4
filesystem mounted at `/cocos`
The final image is written to:
```bash
output/images/disk.img
```
The root filesystem image is also available separately as:
```bash
output/images/rootfs.ext4
```
## Current Boot Flow
At boot, GRUB loads:
- `bzImage`
- `initrd.cpio.gz`
The initramfs script in
[board/rootfs-overlay/init](./board/rootfs-overlay/init)
then:
1. mounts `/proc`, `/sys`, `devtmpfs`, and `devpts`
2. assumes the boot disk is `/dev/sda`
3. opens a dm-verity mapping for the root filesystem using:
- `/dev/sda2` as the data partition
- `/dev/sda3` as the verity hash partition
- `roothash=` from the kernel command line
4. mounts `/dev/mapper/root_verity` read-only at `/root`
5. generates a fresh ephemeral key
6. formats `/dev/sda4` as LUKS2
7. opens it as `/dev/mapper/cocos_crypt`
8. formats that mapper as ext4 and mounts it at `/root/cocos`
9. creates working directories on `/cocos`, including:
- `/cocos/.cache/oci`
- `/cocos/datasets`
- `/cocos/docker`
- `/cocos/cocos_init`
10. mounts `tmpfs` on `/tmp` and `/var` because the root filesystem is
intentionally read-only
11. bind-mounts `/cocos/docker` onto `/var/lib/docker`
12. bind-mounts `/cocos/cocos_init` onto `/cocos_init`
13. rewrites `/etc/fstab` in the mounted root to describe the live runtime
14. preserves or adds 9P mounts for:
- `certs_share` -> `/etc/certs`
- `env_share` -> `/etc/cocos`
15. securely wipes the temporary LUKS key file
16. runs `switch_root /root /sbin/init`
Important details:
- the root filesystem is verified through dm-verity before it is mounted
- `/cocos` is encrypted with an ephemeral per-boot key
- that key is not persisted, so `/cocos` is provisioned fresh on each boot
## Runtime Filesystem Model
The running system is split into:
- read-only root on `/`
- encrypted writable storage on `/cocos`
- `tmpfs` on `/tmp`
- `tmpfs` on `/var`
Service state that must survive within a boot session is redirected away from
the read-only root:
- Docker data lives on `/cocos/docker`
- agent setup scripts work through `/cocos_init`, which is backed by
`/cocos/cocos_init`
- algorithm datasets and results live under `/cocos`
This means services can use `/cocos` like a regular directory tree after boot,
even though it is backed by an encrypted mapper created in early userspace.
## systemd Runtime Expectations
Several services depend on files mounted from 9P shares under `/etc/certs` and
`/etc/cocos`. To avoid boot-order races, the rootfs overlay includes systemd
drop-ins under:
```bash
board/rootfs-overlay/usr/lib/systemd/system/*service.d/
```
These drop-ins require the relevant mount points before starting services such
as:
- `egress-proxy.service`
- `log-forwarder.service`
- `computation-runner.service`
- `cocos-agent.service`
The overlay also ships tmpfiles rules in
[board/rootfs-overlay/usr/lib/tmpfiles.d/cocos.conf](./board/rootfs-overlay/usr/lib/tmpfiles.d/cocos.conf)
to create:
- `/var/log/cocos`
- `/run/cocos`
## Agent Packaging In Buildroot
The Buildroot `agent` package is wired to build the binary from the local Cocos
checkout, not only from a downloaded release snapshot. The package definition is
in [package/agent/agent.mk](./package/agent/agent.mk).
That package currently:
- builds `cocos-agent` from the local source tree
- installs the local
[cocos-agent.service](../../init/systemd/cocos-agent.service)
- installs the local
[agent_setup.sh](../../init/systemd/agent_setup.sh)
- installs the local
[agent_start_script.sh](../../init/systemd/agent_start_script.sh)
So changes under:
- `cocos/agent/...`
- `cocos/init/systemd/...`
are intended to be picked up by the next Buildroot rebuild.
## Buildroot Packages And Tools
The current `cocos_defconfig` includes the components needed by the boot flow
and runtime image, including:
- systemd
- DHCP client
- `cryptsetup`
- `eudev`
- `e2fsprogs`
- Docker, containerd, and runc
- `skopeo`
- TPM2 tools
- 9P filesystem support
- GRUB2 EFI boot support
- host `genimage`
The initramfs built in `post-image.sh` is intentionally minimal and contains
only the binaries needed for early boot, dm-verity root verification, and
`/cocos` provisioning.
## Secure Boot Notes
During `post-image.sh`:
- GRUB is rebuilt with `--disable-shim-lock`
- `bootx64.efi` and `bzImage` are signed with the configured Secure Boot keys
when those keys are present
This flow is designed for booting directly through OVMF with your own enrolled
keys. It does not currently rely on booting through `shim`.
## Rebuilding
This directory is meant to be used as a Buildroot external tree. From this
directory, configure a Buildroot checkout with:
```bash
make -C /path/to/buildroot BR2_EXTERNAL=$PWD cocos_defconfig
```
Then build with:
```bash
make -C /path/to/buildroot BR2_EXTERNAL=$PWD -j$(nproc)
```
The resulting boot image is:
```bash
/path/to/buildroot/output/images/disk.img
```
Additional generated artifacts include:
```bash
/path/to/buildroot/output/images/rootfs.ext4
/path/to/buildroot/output/images/rootfs.verity
/path/to/buildroot/output/images/rootfs.roothash
```
+42
View File
@@ -0,0 +1,42 @@
image efi-part.vfat {
vfat {
file EFI {
image = "efi-part/EFI"
}
file bzImage {
image = "efi-part/bzImage"
}
file initrd.cpio.gz {
image = "efi-part/initrd.cpio.gz"
}
}
size = 256M
}
image disk.img {
hdimage {
partition-table-type = "gpt"
}
partition efi {
image = "efi-part.vfat"
partition-type-uuid = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
offset = 1M
bootable = true
}
partition root {
image = "rootfs.ext4"
partition-type-uuid = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
}
partition verity {
image = "rootfs.verity"
partition-type-uuid = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
}
partition cocos {
size = "20480M"
partition-type-uuid = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
}
}
+279
View File
@@ -0,0 +1,279 @@
###
# Architecture / base
###
CONFIG_SYSVIPC=y
CONFIG_SMP=y
CONFIG_EXPERT=y
CONFIG_LOCALVERSION_AUTO=n
###
# Modules
###
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
###
# Virtualization
###
CONFIG_HYPERVISOR_GUEST=y
CONFIG_PARAVIRT=y
CONFIG_VIRTUALIZATION=y
CONFIG_KVM=y
CONFIG_KVM_SW_PROTECTED_VM=y
CONFIG_KVM_INTEL=y
CONFIG_VIRT_DRIVERS=y
###
# Cgroups — base + Docker/container subsystems
###
CONFIG_CGROUPS=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_MISC=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_BPF=y
CONFIG_CGROUP_NET_PRIO=y
CONFIG_CGROUP_NET_CLASSID=y
CONFIG_CPUSETS=y
CONFIG_MEMCG=y
CONFIG_BLK_CGROUP=y
###
# Namespaces — required by containerd / runc
###
CONFIG_NAMESPACES=y
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_USER_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
###
# PCI
###
CONFIG_PCI=y
CONFIG_PCI_MSI=y
CONFIG_IRQ_REMAP=y
###
# Initramfs
###
CONFIG_BLK_DEV_INITRD=y
CONFIG_RD_GZIP=y
###
# Block devices
###
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_BLK_DEV_SD=y
CONFIG_SCSI_VIRTIO=y
CONFIG_ATA=y
CONFIG_ATA_PIIX=y
CONFIG_VIRTIO_BLK=y
# Loop device (used by containerd image mounts)
CONFIG_BLK_DEV_LOOP=y
CONFIG_BLK_DEV_LOOP_MIN_COUNT=8
###
# Device mapper — FDE, dm-verity, dm-crypt, dm-integrity
# These must be built-in (y) because they are needed before the
# rootfs is mounted, during the initramfs FDE init stage.
###
CONFIG_MD=y
CONFIG_BLK_DEV_DM_BUILTIN=y
CONFIG_BLK_DEV_DM=y
CONFIG_DM_CRYPT=y
CONFIG_DM_VERITY=y
CONFIG_DM_VERITY_VERIFY_ROOTHASH_SIG=y
# CONFIG_DM_VERITY_FEC is not set
CONFIG_DM_INTEGRITY=y
CONFIG_DM_INIT=y
###
# Networking — base
###
CONFIG_NET=y
CONFIG_PACKET=y
CONFIG_UNIX=y
CONFIG_INET=y
# CONFIG_WIRELESS is not set
CONFIG_NETDEVICES=y
CONFIG_VIRTIO_NET=y
CONFIG_NE2K_PCI=y
CONFIG_8139CP=y
# CONFIG_WLAN is not set
CONFIG_VSOCKETS=y
CONFIG_VIRTIO_VSOCKETS=y
# Virtual Ethernet pairs and bridge (Docker networking)
CONFIG_VETH=m
CONFIG_BRIDGE=m
CONFIG_BRIDGE_NETFILTER=m
###
# Netfilter — Docker NAT, iptables, conntrack (modules, loaded on demand)
###
CONFIG_NETFILTER=y
CONFIG_NETFILTER_ADVANCED=y
CONFIG_NF_CONNTRACK=m
CONFIG_NF_CONNTRACK_MARK=y
CONFIG_NF_NAT=m
CONFIG_NF_NAT_MASQUERADE=y
CONFIG_NF_TABLES=y
CONFIG_IP_NF_IPTABLES=m
CONFIG_IP_NF_FILTER=m
CONFIG_IP_NF_TARGET_MASQUERADE=m
CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=m
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=m
###
# BPF
###
CONFIG_BPF_SYSCALL=y
###
# Filesystems
###
CONFIG_EXT4_FS=y
CONFIG_OVERLAY_FS=y
CONFIG_AUTOFS4_FS=y
CONFIG_TMPFS=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_PROC_FS=y
CONFIG_SYSFS=y
###
# 9P filesystem (virtio shares for certs and env)
###
CONFIG_NET_9P=y
CONFIG_NET_9P_VIRTIO=y
CONFIG_9P_FS=y
CONFIG_9P_FS_POSIX_ACL=y
CONFIG_9P_FS_SECURITY=y
###
# Virtio devices
###
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_BALLOON=y
CONFIG_VIRTIO_INPUT=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_VIRTIO_MMIO=y
CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y
CONFIG_HW_RANDOM_VIRTIO=m
###
# Console / Input
###
CONFIG_INPUT_EVDEV=y
CONFIG_SERIAL_8250=y
CONFIG_SERIAL_8250_CONSOLE=y
###
# Kernel features required by systemd
###
CONFIG_FHANDLE=y
CONFIG_INOTIFY_USER=y
CONFIG_SIGNALFD=y
CONFIG_TIMERFD=y
CONFIG_EPOLL=y
CONFIG_POSIX_MQUEUE=y
CONFIG_POSIX_MQUEUE_SYSCTL=y
CONFIG_UNWINDER_FRAME_POINTER=y
###
# Security
###
CONFIG_SECCOMP=y
CONFIG_SECCOMP_FILTER=y
CONFIG_SECURITY=y
CONFIG_SECURITYFS=y
###
# EFI
###
CONFIG_EFI=y
CONFIG_EFI_STUB=y
###
# AMD SEV-SNP
###
CONFIG_AMD_MEM_ENCRYPT=y
CONFIG_AMD_MEM_ENCRYPT_ACTIVE_BY_DEFAULT=n
CONFIG_SEV_GUEST=y
CONFIG_IOMMU_DEFAULT_PASSTHROUGH=n
###
# Intel TDX
###
CONFIG_X86_X2APIC=y
CONFIG_X86_CPUID=y
CONFIG_X86_SGX=y
CONFIG_X86_SGX_KVM=y
CONFIG_INTEL_TDX_GUEST=y
CONFIG_TDX_GUEST_DRIVER=y
###
# Preemption (disabled for VM performance)
###
CONFIG_PREEMPT_COUNT=n
CONFIG_PREEMPT=n
CONFIG_PREEMPT_DYNAMIC=n
CONFIG_DEBUG_PREEMPT=n
###
# Key/signature management
###
CONFIG_SYSTEM_TRUSTED_KEYS=n
CONFIG_SYSTEM_REVOCATION_KEYS=n
CONFIG_MODULE_SIG_KEY=n
CONFIG_KEYS=y
CONFIG_ENCRYPTED_KEYS=y
###
# Crypto — AES-GCM (LUKS2 cipher) + SHA-256 (dm-verity hash)
###
CONFIG_CRYPTO_AES=y
CONFIG_CRYPTO_SHA256=y
CONFIG_CRYPTO_GCM=y
CONFIG_CRYPTO_GHASH=y
CONFIG_CRYPTO_SEQIV=y
CONFIG_CRYPTO_ECHAINIV=y
CONFIG_CRYPTO_XTS=y
CONFIG_CRYPTO_CBC=y
CONFIG_CRYPTO_AUTHENC=y
CONFIG_CRYPTO_ESSIV=y
CONFIG_CRYPTO_USER_API=y
CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_CRYPTO_USER_API_SKCIPHER=y
CONFIG_CRYPTO_USER_API_AEAD=y
CONFIG_CRYPTO_AES_NI_INTEL=m
CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=m
###
# TPM
###
CONFIG_TCG_TPM=y
CONFIG_TCG_TPM2_HMAC=y
CONFIG_TCG_PLATFORM=y
###
# IMA (Linux Integrity Measurement Architecture)
###
CONFIG_INTEGRITY=y
CONFIG_INTEGRITY_SIGNATURE=y
CONFIG_IMA=y
CONFIG_IMA_MEASURE_PCR_IDX=10
CONFIG_IMA_LSM_RULES=y
CONFIG_IMA_APPRAISE=y
CONFIG_IMA_DEFAULT_TEMPLATE="ima-ng"
CONFIG_IMA_DEFAULT_HASH="sha256"
###
# Disabled options
###
CONFIG_KSM=n
CONFIG_EISA=n
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -u
set -e
# Add a console on tty1
if [ -e ${TARGET_DIR}/etc/inittab ]; then
grep -qE '^tty1::' ${TARGET_DIR}/etc/inittab || \
sed -i '/GENERIC_SERIAL/a\
tty1::respawn:/sbin/getty -L tty1 0 vt100 # QEMU graphical window' ${TARGET_DIR}/etc/inittab
fi
+273
View File
@@ -0,0 +1,273 @@
#!/bin/bash
COCOS_BOARD_DIR="$(dirname "$0")"
DEFCONFIG_NAME="$(basename "$2")"
README_FILES="${COCOS_BOARD_DIR}/readme.txt"
START_QEMU_SCRIPT="${BINARIES_DIR}/start-qemu.sh"
# ---------------------------------------------------------------------------
# Build a minimal FDE initramfs (rootfs.cpio.gz) containing only the tools
# needed to mount the root partition read-only, provision LUKS2, and switch_root.
# All other packages live
# on the ext4 disk image and are available after switch_root.
# ---------------------------------------------------------------------------
echo "[post-image] Building minimal FDE initramfs..."
INITRAMFS_STAGE="${BUILD_DIR}/initramfs-staging"
rm -rf "${INITRAMFS_STAGE}"
# Merged-usr layout: bin/sbin/lib/lib64 are symlinks into usr/, matching the
# Buildroot target layout so that hardcoded ELF interpreter paths (ld-linux)
# and the #!/bin/sh shebang both resolve correctly inside the initramfs.
mkdir -p "${INITRAMFS_STAGE}/usr/bin" \
"${INITRAMFS_STAGE}/usr/sbin" \
"${INITRAMFS_STAGE}/usr/lib" \
"${INITRAMFS_STAGE}/dev" \
"${INITRAMFS_STAGE}/proc" \
"${INITRAMFS_STAGE}/sys" \
"${INITRAMFS_STAGE}/tmp" \
"${INITRAMFS_STAGE}/run" \
"${INITRAMFS_STAGE}/root" \
"${INITRAMFS_STAGE}/etc/udev/rules.d"
ln -s usr/bin "${INITRAMFS_STAGE}/bin"
ln -s usr/sbin "${INITRAMFS_STAGE}/sbin"
ln -s usr/lib "${INITRAMFS_STAGE}/lib"
ln -s usr/lib "${INITRAMFS_STAGE}/lib64"
# init script (PID 1)
install -m 0755 "${BR2_EXTERNAL_COCOS_PATH}/board/rootfs-overlay/init" \
"${INITRAMFS_STAGE}/init"
# Binaries required by the init script
FDE_BINS="
bash
cryptsetup
veritysetup
mkfs.ext4
mount
umount
losetup
switch_root
dd
shred
tr
cut
grep
awk
cat
ls
cp
mkdir
readlink
dirname
lsblk
udevadm
blkid
rm
"
for BIN in ${FDE_BINS}; do
SRC="$(find "${TARGET_DIR}/usr/bin" "${TARGET_DIR}/usr/sbin" \
"${TARGET_DIR}/bin" "${TARGET_DIR}/sbin" \
-name "${BIN}" \( -type f -o -type l \) 2>/dev/null | head -1)"
if [ -n "${SRC}" ]; then
cp -P "${SRC}" "${INITRAMFS_STAGE}/usr/bin/${BIN}"
chmod 0755 "${INITRAMFS_STAGE}/usr/bin/${BIN}" 2>/dev/null || true
# If this is a symlink, also copy the resolved target binary (e.g. busybox, coreutils, mke2fs)
# so that other applet symlinks pointing to the same target also work at runtime.
if [ -L "${SRC}" ]; then
REAL_SRC="$(readlink -f "${SRC}")"
REAL_NAME="$(basename "${REAL_SRC}")"
if [ -f "${REAL_SRC}" ] && [ ! -e "${INITRAMFS_STAGE}/usr/bin/${REAL_NAME}" ]; then
cp "${REAL_SRC}" "${INITRAMFS_STAGE}/usr/bin/${REAL_NAME}"
chmod 0755 "${INITRAMFS_STAGE}/usr/bin/${REAL_NAME}" 2>/dev/null || true
fi
fi
else
echo "[post-image] WARNING: ${BIN} not found in target, skipping"
fi
done
# sh symlink so #!/bin/sh in the init script resolves correctly
ln -sf bash "${INITRAMFS_STAGE}/usr/bin/sh"
# Shared libraries from usr/lib (TARGET_DIR uses merged-usr so lib → usr/lib)
# Skip large runtimes that are only needed on the real root.
find "${TARGET_DIR}/usr/lib" \( \
-path "*/python3*" -o \
-path "*/gcc*" -o \
-path "*/wasmedge*" \
\) -prune -o \
\( -name "*.so" -o -name "*.so.*" \) -print | while read -r LIB; do
REL="${LIB#${TARGET_DIR}/usr/lib/}"
DEST="${INITRAMFS_STAGE}/usr/lib/${REL}"
mkdir -p "$(dirname "${DEST}")"
cp -P "${LIB}" "${DEST}"
done
# udev rules (needed for udevadm settle)
if [ -d "${TARGET_DIR}/etc/udev" ]; then
cp -a "${TARGET_DIR}/etc/udev/." "${INITRAMFS_STAGE}/etc/udev/"
fi
# /dev seed nodes
mknod -m 0600 "${INITRAMFS_STAGE}/dev/console" c 5 1 2>/dev/null || true
mknod -m 0666 "${INITRAMFS_STAGE}/dev/null" c 1 3 2>/dev/null || true
echo "[post-image] Packing initramfs..."
( cd "${INITRAMFS_STAGE}" && \
find . | cpio --quiet -o -H newc -R 0:0 | gzip -9 \
> "${BINARIES_DIR}/rootfs.cpio.gz" )
echo "[post-image] rootfs.cpio.gz: $(du -sh "${BINARIES_DIR}/rootfs.cpio.gz" | cut -f1)"
ROOTFS_IMAGE="${BINARIES_DIR}/rootfs.ext4"
VERITY_IMAGE="${BINARIES_DIR}/rootfs.verity"
ROOT_HASH_FILE="${BINARIES_DIR}/rootfs.roothash"
VERITYSETUP_BIN="${HOST_DIR}/bin/veritysetup"
if [ ! -x "${VERITYSETUP_BIN}" ]; then
VERITYSETUP_BIN="${HOST_DIR}/sbin/veritysetup"
fi
if [ ! -x "${VERITYSETUP_BIN}" ]; then
echo "[post-image] FATAL: host veritysetup not found at ${VERITYSETUP_BIN}"
exit 1
fi
echo "[post-image] Building dm-verity hash image..."
rm -f "${VERITY_IMAGE}" "${ROOT_HASH_FILE}"
truncate -s 256M "${VERITY_IMAGE}"
VERITY_FORMAT_OUTPUT="$("${VERITYSETUP_BIN}" format "${ROOTFS_IMAGE}" "${VERITY_IMAGE}")" || {
echo "[post-image] FATAL: veritysetup format failed"
exit 1
}
ROOT_HASH="$(printf '%s\n' "${VERITY_FORMAT_OUTPUT}" | awk -F': ' '/^Root hash:/ {print $2}' | tr -d '[:space:]')"
if [ -z "${ROOT_HASH}" ]; then
echo "[post-image] FATAL: failed to parse dm-verity root hash"
printf '%s\n' "${VERITY_FORMAT_OUTPUT}"
exit 1
fi
printf '%s\n' "${ROOT_HASH}" > "${ROOT_HASH_FILE}"
echo "[post-image] dm-verity root hash: ${ROOT_HASH}"
# Stage kernel and initramfs for the EFI partition.
# Buildroot's GRUB2 package has already placed bootx64.efi at
# ${BINARIES_DIR}/efi-part/EFI/BOOT/bootx64.efi; we add the kernel,
# initramfs, and overwrite the default grub.cfg with our boot entry.
echo "[post-image] Staging EFI partition files..."
mkdir -p "${BINARIES_DIR}/efi-part/EFI/BOOT"
cp "${BINARIES_DIR}/bzImage" "${BINARIES_DIR}/efi-part/bzImage"
cp "${BINARIES_DIR}/rootfs.cpio.gz" "${BINARIES_DIR}/efi-part/initrd.cpio.gz"
cat > "${BINARIES_DIR}/efi-part/EFI/BOOT/grub.cfg" << GRUBCFG
set default=0
set timeout=0
menuentry "Cocos" {
linux /bzImage console=ttyS0 roothash=${ROOT_HASH} systemd.verity=0 systemd.gpt_auto=0
initrd /initrd.cpio.gz
}
GRUBCFG
# Regenerate bootx64.efi with --disable-shim-lock so GRUB can load the kernel
# directly without requiring the shim bootloader (OVMF still verifies GRUB via
# Secure Boot; shim is not needed when booting from a custom OVMF with own DB key).
GRUB_CORE="$(ls -d "${BUILD_DIR}"/grub2-*/build-x86_64-efi/grub-core 2>/dev/null | head -1)"
if [ -n "${GRUB_CORE}" ]; then
echo "[post-image] Regenerating bootx64.efi with --disable-shim-lock..."
"${HOST_DIR}/bin/grub-mkimage" \
-d "${GRUB_CORE}" \
-O x86_64-efi \
-o "${BINARIES_DIR}/efi-part/EFI/BOOT/bootx64.efi" \
-p "/EFI/BOOT" \
--disable-shim-lock \
boot linux echo normal part_gpt fat ls search || {
echo "[post-image] FATAL: grub-mkimage failed"
exit 1
}
else
echo "[post-image] WARNING: GRUB core dir not found, skipping --disable-shim-lock rebuild"
fi
# Sign GRUB and kernel for UEFI Secure Boot.
# Keys are resolved in order: env var → board/secure-boot/ defaults.
SB_KEY="${SB_KEY:-${COCOS_BOARD_DIR}/secure-boot/db.key}"
SB_CERT="${SB_CERT:-${COCOS_BOARD_DIR}/secure-boot/db.crt}"
if [ -f "${SB_KEY}" ] && [ -f "${SB_CERT}" ]; then
echo "[post-image] Signing EFI binaries for Secure Boot..."
sbsign --key "${SB_KEY}" --cert "${SB_CERT}" \
--output "${BINARIES_DIR}/efi-part/EFI/BOOT/bootx64.efi" \
"${BINARIES_DIR}/efi-part/EFI/BOOT/bootx64.efi" || {
echo "[post-image] FATAL: Failed to sign bootx64.efi"
exit 1
}
sbsign --key "${SB_KEY}" --cert "${SB_CERT}" \
--output "${BINARIES_DIR}/efi-part/bzImage" \
"${BINARIES_DIR}/efi-part/bzImage" || {
echo "[post-image] FATAL: Failed to sign bzImage"
exit 1
}
echo "[post-image] Secure Boot signing complete"
else
echo "[post-image] WARNING: Secure Boot keys not found — EFI binaries are unsigned"
echo "[post-image] Default location: ${COCOS_BOARD_DIR}/secure-boot/db.key + db.crt"
echo "[post-image] Override: SB_KEY=/path/to/db.key SB_CERT=/path/to/db.crt make"
fi
GENIMAGE_CFG="${COCOS_BOARD_DIR}/genimage.cfg"
if [ -f "${GENIMAGE_CFG}" ]; then
GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp"
rm -rf "${GENIMAGE_TMP}"
genimage \
--rootpath "${TARGET_DIR}" \
--tmppath "${GENIMAGE_TMP}" \
--inputpath "${BINARIES_DIR}" \
--outputpath "${BINARIES_DIR}" \
--config "${GENIMAGE_CFG}"
fi
if [[ "${DEFCONFIG_NAME}" =~ ^"cocos_*" ]]; then
# Not a Qemu defconfig, can't test.
exit 0
fi
# Search for "# qemu_*_defconfig" tag in all readme.txt files.
# Qemu command line on multilines using back slash are accepted.
# shellcheck disable=SC2086 # glob over each readme file
QEMU_CMD_LINE="$(sed -r ':a; /\\$/N; s/\\\n//; s/\t/ /; ta; /# '"${DEFCONFIG_NAME}"'$/!d; s/#.*//' ${README_FILES})"
if [ -z "${QEMU_CMD_LINE}" ]; then
# No Qemu cmd line found, can't test.
exit 0
fi
# Remove output/images path since the script will be in
# the same directory as the kernel and the rootfs images.
QEMU_CMD_LINE="${QEMU_CMD_LINE//output\/images\//}"
# Remove -serial stdio if present, keep it as default args
DEFAULT_ARGS="$(sed -r -e '/-serial stdio/!d; s/.*(-serial stdio).*/\1/' <<<"${QEMU_CMD_LINE}")"
QEMU_CMD_LINE="${QEMU_CMD_LINE//-serial stdio/}"
# Remove any string before qemu-system-*
QEMU_CMD_LINE="$(sed -r -e 's/^.*(qemu-system-)/\1/' <<<"${QEMU_CMD_LINE}")"
# Disable graphical output and redirect serial I/Os to console
case ${DEFCONFIG_NAME} in
(qemu_sh4eb_r2d_defconfig|qemu_sh4_r2d_defconfig)
# Special case for SH4
SERIAL_ARGS="-serial stdio -display none"
;;
(*)
SERIAL_ARGS="-nographic"
;;
esac
sed -e "s|@SERIAL_ARGS@|${SERIAL_ARGS}|g" \
-e "s|@DEFAULT_ARGS@|${DEFAULT_ARGS}|g" \
-e "s|@QEMU_CMD_LINE@|${QEMU_CMD_LINE}|g" \
-e "s|@HOST_DIR@|${HOST_DIR}|g" \
<"${COCOS_BOARD_DIR}/start-qemu.sh.in" \
>"${START_QEMU_SCRIPT}"
chmod +x "${START_QEMU_SCRIPT}"
+7
View File
@@ -0,0 +1,7 @@
Run the emulation with:
qemu-system-x86_64 -M pc -kernel output/images/bzImage -drive file=output/images/rootfs.ext2,if=virtio,format=raw -append "rootwait root=/dev/vda console=tty1 console=ttyS0" -serial stdio -net nic,model=virtio -net user # cocos_defconfig
Optionally add -smp N to emulate a SMP system with N CPUs.
The login prompt will appear in the graphical window.
@@ -0,0 +1,3 @@
# Private key must not be committed
db.key
db.crt
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
BINARIES_DIR="${0%/*}/"
# shellcheck disable=SC2164
cd "${BINARIES_DIR}"
mode_serial=false
mode_sys_qemu=false
while [ "$1" ]; do
case "$1" in
--serial-only|serial-only) mode_serial=true; shift;;
--use-system-qemu) mode_sys_qemu=true; shift;;
--) shift; break;;
*) echo "unknown option: $1" >&2; exit 1;;
esac
done
if ${mode_serial}; then
EXTRA_ARGS='@SERIAL_ARGS@'
else
EXTRA_ARGS='@DEFAULT_ARGS@'
fi
if ! ${mode_sys_qemu}; then
export PATH="@HOST_DIR@/bin:${PATH}"
fi
exec @QEMU_CMD_LINE@ ${EXTRA_ARGS} "$@"
+7
View File
@@ -0,0 +1,7 @@
# Root is mounted read-only by the initramfs through dm-verity.
# /cocos, /var, /tmp, and bind mounts are set up by the initramfs init script.
/dev/mapper/root_verity / ext4 ro,defaults 0 0
# 9P virtio shares — provided by the hypervisor, optional (nofail)
certs_share /etc/certs 9p trans=virtio,version=9p2000.L,cache=mmap,nofail 0 0
env_share /etc/cocos 9p trans=virtio,version=9p2000.L,cache=mmap,nofail 0 0
@@ -0,0 +1,7 @@
{
"key-providers": {
"attestation-agent": {
"grpc": "127.0.0.1:50011"
}
}
}
+265
View File
@@ -0,0 +1,265 @@
#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi
echo "Welcome to the Cocos FDE test VM initramfs!"
echo "This is a minimal initramfs environment used for testing the FDE provisioning flow."
echo "If you see this message, the initramfs was loaded and executed successfully."
echo "The initramfs will now attempt to provision the disk and mount the real root filesystem."
echo "If any step fails, it will drop to a shell for debugging."
[ -d /dev ] || mkdir -m 0755 /dev
[ -d /etc ] || mkdir -m 0755 /etc
[ -d /root ] || mkdir -m 0700 /root
[ -d /run ] || mkdir -m 0755 /run
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
if [ -L /etc/resolv.conf ]; then
RESOLV_TARGET="$(readlink /etc/resolv.conf)"
case "$RESOLV_TARGET" in
/*)
RESOLV_PATH="$RESOLV_TARGET"
;;
*)
RESOLV_PATH="/etc/$RESOLV_TARGET"
;;
esac
mkdir -p "$(dirname "$RESOLV_PATH")"
[ -e "$RESOLV_PATH" ] || : > "$RESOLV_PATH"
else
[ -e /etc/resolv.conf ] || : > /etc/resolv.conf
fi
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
mkdir -p /sys/kernel/config
if ! grep -q ' /sys/kernel/config ' /proc/mounts; then
mount -t configfs configfs /sys/kernel/config 2>/dev/null || true
fi
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
mkdir /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
MNT_DIR=/root
BASE=$(pwd)
DST=/dev/sda
ROOTFS_TYPE="ext4"
ROOT_VERITY_MAP=root_verity
ROOT_VERITY_MAPPER="/dev/mapper/$ROOT_VERITY_MAP"
COCOS_MOUNT=/cocos
COCOS_MAP=cocos_crypt
COCOS_MAPPER="/dev/mapper/$COCOS_MAP"
LUKS_PARAMS="--cipher aes-gcm-random --integrity aead"
settle_devices() {
echo "[init] Waiting for devices to settle..."
if command -v udevadm >/dev/null 2>&1; then
udevadm settle --timeout=10 || sleep 2
else
sleep 2
fi
}
wipe_file() {
file_path="$1"
if [ -z "$file_path" ] || [ ! -e "$file_path" ]; then
return 0
fi
shred -vfz -n 3 "$file_path" 2>/dev/null || dd if=/dev/zero of="$file_path" bs=64 count=1
rm -f "$file_path"
}
partition_path() {
disk="$1"
partition="$2"
case "$disk" in
*[0-9])
printf '%sp%s\n' "$disk" "$partition"
;;
*)
printf '%s%s\n' "$disk" "$partition"
;;
esac
}
append_9p_entry() {
pattern="$1"
default_entry="$2"
existing_entry=""
if [ -f "$FSTAB_BAK" ]; then
existing_entry="$(grep -E "$pattern" "$FSTAB_BAK" | head -n 1 || true)"
fi
if [ -n "$existing_entry" ]; then
printf '%s\n' "$existing_entry" >> "$FSTAB"
else
printf '%s\n' "$default_entry" >> "$FSTAB"
fi
}
cmdline_arg() {
key="$1"
for arg in $(cat /proc/cmdline); do
case "$arg" in
"$key="*)
printf '%s\n' "${arg#*=}"
return 0
;;
esac
done
return 1
}
echo "[init] Starting disk provisioning..."
ROOT_PART="$(partition_path "$DST" 2)"
VERITY_PART="$(partition_path "$DST" 3)"
COCOS_PART="$(partition_path "$DST" 4)"
ROOT_HASH="$(cmdline_arg roothash)"
if [ -z "$ROOT_HASH" ]; then
echo "[init] FATAL: Missing roothash= on kernel command line"
exec /bin/sh
fi
settle_devices
for part in "$ROOT_PART" "$VERITY_PART" "$COCOS_PART"; do
if [ ! -b "$part" ]; then
echo "[init] FATAL: Could not find partition $part"
echo "[init] Available block devices:"
lsblk || ls -la /dev/ || true
echo "[init] Dropping to shell."
exec /bin/sh
fi
done
echo "[init] Opening dm-verity root mapping..."
veritysetup open "$ROOT_PART" "$ROOT_VERITY_MAP" "$VERITY_PART" "$ROOT_HASH" || {
echo "[init] FATAL: Failed to open dm-verity mapping for root"
exec /bin/sh
}
echo "[init] Mounting root at $MNT_DIR (read-only)..."
mount -o ro -t "$ROOTFS_TYPE" "$ROOT_VERITY_MAPPER" "$MNT_DIR" || {
echo "[init] FATAL: Failed to mount verity root"
veritysetup close "$ROOT_VERITY_MAP" 2>/dev/null || true
exec /bin/sh
}
echo "[init] Generating ephemeral key for $COCOS_MOUNT..."
dd if=/dev/urandom of=kk.bin bs=64 count=1 || {
echo "[init] FATAL: Failed to generate encryption key"
umount "$MNT_DIR" 2>/dev/null || true
exec /bin/sh
}
KK_BIN=$BASE/kk.bin
cryptsetup luksFormat "$COCOS_PART" --type luks2 $LUKS_PARAMS --key-file="$KK_BIN" -q || {
echo "[init] FATAL: LUKS format failed"
umount "$MNT_DIR" 2>/dev/null || true
veritysetup close "$ROOT_VERITY_MAP" 2>/dev/null || true
wipe_file "$KK_BIN"
exec /bin/sh
}
cryptsetup open "$COCOS_PART" "$COCOS_MAP" --key-file="$KK_BIN" || {
echo "[init] FATAL: Failed to open LUKS container for $COCOS_MOUNT"
umount "$MNT_DIR" 2>/dev/null || true
veritysetup close "$ROOT_VERITY_MAP" 2>/dev/null || true
wipe_file "$KK_BIN"
exec /bin/sh
}
mkfs.ext4 -F -m 0 "$COCOS_MAPPER" >/dev/null || {
echo "[init] FATAL: Failed to create ext4 filesystem for $COCOS_MOUNT"
cryptsetup close "$COCOS_MAP" 2>/dev/null || true
umount "$MNT_DIR" 2>/dev/null || true
veritysetup close "$ROOT_VERITY_MAP" 2>/dev/null || true
wipe_file "$KK_BIN"
exec /bin/sh
}
echo "[init] Mounting encrypted $COCOS_MOUNT..."
mkdir -p "$MNT_DIR$COCOS_MOUNT"
mount -t ext4 "$COCOS_MAPPER" "$MNT_DIR$COCOS_MOUNT" || {
echo "[init] FATAL: Failed to mount encrypted $COCOS_MOUNT filesystem"
cryptsetup close "$COCOS_MAP" 2>/dev/null || true
umount "$MNT_DIR" 2>/dev/null || true
veritysetup close "$ROOT_VERITY_MAP" 2>/dev/null || true
wipe_file "$KK_BIN"
exec /bin/sh
}
mkdir -p \
"$MNT_DIR$COCOS_MOUNT/.cache/oci" \
"$MNT_DIR$COCOS_MOUNT/datasets" \
"$MNT_DIR$COCOS_MOUNT/docker" \
"$MNT_DIR$COCOS_MOUNT/cocos_init"
# The root is read-only; provide tmpfs for writable system directories.
mount -t tmpfs tmpfs "$MNT_DIR/tmp"
mount -t tmpfs -o mode=0755 tmpfs "$MNT_DIR/var"
# Bind Docker's data root onto /cocos so large images don't exhaust RAM.
mkdir -p "$MNT_DIR/var/lib/docker"
mount --bind "$MNT_DIR$COCOS_MOUNT/docker" "$MNT_DIR/var/lib/docker"
# /cocos_init is on the read-only root; shadow it with a writable
# copy on /cocos so agent setup scripts can write state alongside the scripts.
if [ -d "$MNT_DIR/cocos_init" ]; then
cp -a "$MNT_DIR/cocos_init/." "$MNT_DIR$COCOS_MOUNT/cocos_init/" 2>/dev/null || true
mount --bind "$MNT_DIR$COCOS_MOUNT/cocos_init" "$MNT_DIR/cocos_init" || true
fi
mount --move /proc $MNT_DIR/proc
mount --move /sys $MNT_DIR/sys
FSTAB="$MNT_DIR/etc/fstab"
FSTAB_BAK="$MNT_DIR/etc/fstab.bak"
mkdir -p "$MNT_DIR/etc/certs" "$MNT_DIR/etc/cocos" 2>/dev/null || true
if [ -f "$FSTAB" ]; then
mv "$FSTAB" "$FSTAB_BAK"
fi
cat > "$FSTAB" << EOF
# Generated by init script
$ROOT_VERITY_MAPPER / $ROOTFS_TYPE ro,defaults 0 0
EOF
append_9p_entry \
'^certs_share[[:space:]]+/etc/certs[[:space:]]+9p([[:space:]]|$)' \
'certs_share /etc/certs 9p trans=virtio,version=9p2000.L,cache=mmap,nofail 0 0'
append_9p_entry \
'^env_share[[:space:]]+/etc/cocos[[:space:]]+9p([[:space:]]|$)' \
'env_share /etc/cocos 9p trans=virtio,version=9p2000.L,cache=mmap,nofail 0 0'
printf '%s\n' '# /cocos is mounted by the FDE initramfs using an ephemeral LUKS key.' >> "$FSTAB"
# Securely wipe the encryption key before switching root.
echo "[init] Securely wiping the $COCOS_MOUNT encryption key..."
wipe_file "$KK_BIN"
echo "[init] Switching to real root..."
exec switch_root $MNT_DIR/ /sbin/init
# If switch_root somehow returns:
echo "[init] switch_root failed, dropping to shell"
exec /bin/sh
@@ -0,0 +1,3 @@
[Unit]
RequiresMountsFor=/etc/cocos /etc/certs
@@ -0,0 +1,3 @@
[Unit]
RequiresMountsFor=/etc/cocos
@@ -0,0 +1,3 @@
[Unit]
RequiresMountsFor=/etc/cocos
@@ -0,0 +1,3 @@
[Unit]
RequiresMountsFor=/etc/cocos
@@ -0,0 +1,2 @@
d /var/log/cocos 0755 root root -
d /run/cocos 0755 root root -
+117
View File
@@ -0,0 +1,117 @@
# Architecture
BR2_x86_64=y
# System
BR2_TARGET_GENERIC_HOSTNAME="cocos"
BR2_TARGET_GENERIC_ISSUE="Welcome to Cocos"
BR2_PACKAGE_DHCP=y
BR2_PACKAGE_DHCP_CLIENT=y
BR2_INIT_SYSTEMD=y
BR2_SYSTEM_BIN_SH_BASH=y
# Filesystem
# BR2_TARGET_ROOTFS_TAR is not set
# Initramfs (rootfs.cpio.gz) is built by post-image.sh from only the FDE tools,
# not from the full target rootfs. The full rootfs goes to rootfs.ext4 (disk image).
BR2_ROOTFS_OVERLAY="$(BR2_EXTERNAL_COCOS_PATH)/board/rootfs-overlay"
# Patches for existing Buildroot packages
BR2_GLOBAL_PATCH_DIR="$(BR2_EXTERNAL_COCOS_PATH)/patches"
# Bootloader
BR2_TARGET_GRUB2=y
BR2_TARGET_GRUB2_X86_64_EFI=y
BR2_TARGET_GRUB2_BUILTIN_MODULES_EFI="boot linux echo normal part_gpt fat ls search"
# Disk image
BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_4=y
BR2_TARGET_ROOTFS_EXT2_SIZE="10G"
BR2_PACKAGE_HOST_GENIMAGE=y
BR2_PACKAGE_HOST_CRYPTSETUP=y
# Image
BR2_ROOTFS_POST_BUILD_SCRIPT="$(BR2_EXTERNAL_COCOS_PATH)/board/cocos/post-build.sh"
# Image
BR2_ROOTFS_POST_IMAGE_SCRIPT="$(BR2_EXTERNAL_COCOS_PATH)/board/cocos/post-image.sh"
BR2_ROOTFS_POST_SCRIPT_ARGS="$(BR2_DEFCONFIG)"
# Linux headers same as kernel
BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_6_11=y
BR2_TOOLCHAIN_HEADERS_LATEST=y
BR2_TOOLCHAIN_HEADERS_AT_LEAST="6.11-rc7"
# Kernel
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_GIT=y
BR2_LINUX_KERNEL_CUSTOM_REPO_URL="https://github.com/coconut-svsm/linux.git"
BR2_LINUX_KERNEL_CUSTOM_REPO_VERSION="svsm"
BR2_LINUX_KERNEL_VERSION="svsm"
BR2_LINUX_KERNEL_PATCH=""
BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="$(BR2_EXTERNAL_COCOS_PATH)/board/cocos/linux.config"
BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y
# host-qemu for gitlab testing
BR2_PACKAGE_HOST_QEMU=y
BR2_PACKAGE_HOST_QEMU_SYSTEM_MODE=y
# Python
BR2_PACKAGE_PYTHON3=y
BR2_PACKAGE_PYTHON_PIP=y
BR2_PACKAGE_BZIP2=y
BR2_PACKAGE_XZ=y
BR2_PACKAGE_ZIP=y
BR2_PACKAGE_PYTHON3_ZLIB=y
BR2_PACKAGE_PYTHON3_XZ=y
BR2_PACKAGE_PYTHON3_BZIP2=y
BR2_INSTALL_LIBSTDCPP=y
BR2_TOOLCHAIN_BUILDROOT_CXX=y
BR2_PACKAGE_HOST_GCC_TARGET=y
BR2_TOOLCHAIN_BUILDROOT_LIBSTDCPP=y
BR2_PACKAGE_GCC=y
BR2_PACKAGE_GCC_TARGET=y
BR2_PACKAGE_LIBSTDCPP=y
# FDE
BR2_PACKAGE_NBD=y
BR2_PACKAGE_NBD_CLIENT=y
BR2_PACKAGE_CRYPTSETUP=y
BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y
BR2_PACKAGE_EUDEV=y
BR2_PACKAGE_HAS_UDEV=y
BR2_PACKAGE_MULTIPATH_TOOLS=y
BR2_PACKAGE_UTIL_LINUX_BINARIES=y
BR2_PACKAGE_E2FSPROGS=y
BR2_LINUX_KERNEL_NEEDS_HOST_PAHOLE=y
# TPM2
BR2_PACKAGE_TPM2_TOOLS=y
BR2_PACKAGE_COREUTILS=y
# Docker
BR2_PACKAGE_LIBSECCOMP_ARCH_SUPPORTS=y
BR2_PACKAGE_LIBSECCOMP=y
BR2_PACKAGE_CA_CERTIFICATES=y
BR2_PACKAGE_DOCKER_CLI=y
BR2_PACKAGE_DOCKER_COMPOSE=y
BR2_PACKAGE_DOCKER_ENGINE=y
BR2_PACKAGE_CONTAINERD=y
BR2_PACKAGE_RUNC=y
BR2_PACKAGE_IPTABLES=y
# Skopeo for OCI image handling with CoCo Keyprovider
BR2_PACKAGE_SKOPEO=y
BR2_PACKAGE_GPGME=y
BR2_PACKAGE_LVM2=y
BR2_PACKAGE_LVM2_STANDARD_INSTALL=y
BR2_PACKAGE_9PFS=y
# Host tools
BR2_PACKAGE_HOST_RUSTC=y
BR2_PACKAGE_HOST_RUST_BIN=y
# Cocos AI Packages
BR2_PACKAGE_AGENT=y
# BR2_PACKAGE_CC_ATTESTATION_AGENT is not set
+2
View File
@@ -0,0 +1,2 @@
name: COCOS
desc: External buildroot tree for Cocos AI
+1
View File
@@ -0,0 +1 @@
include $(sort $(wildcard $(BR2_EXTERNAL_COCOS_PATH)/package/*/*.mk))
+13
View File
@@ -0,0 +1,13 @@
config BR2_PACKAGE_AGENT
bool "agent"
default y
select BR2_PACKAGE_ATTESTATION_SERVICE
select BR2_PACKAGE_LOG_FORWARDER
select BR2_PACKAGE_COMPUTATION_RUNNER
select BR2_PACKAGE_INGRESS_PROXY
select BR2_PACKAGE_EGRESS_PROXY
help
Confidential Computing Agent is a state machine capable of
receiving datasets and algorithm, running computations, and
fetching the attestation report from within the
Confidential VM.
+27
View File
@@ -0,0 +1,27 @@
################################################################################
#
# Cocos AI Agent
#
################################################################################
AGENT_VERSION = main
AGENT_SITE = $(call github,ultravioletrs,cocos,$(AGENT_VERSION))
define AGENT_BUILD_CMDS
$(MAKE) -C $(@D) agent EMBED_ENABLED=$(AGENT_EMBED_ENABLED)
endef
define AGENT_INSTALL_TARGET_CMDS
mkdir -p $(TARGET_DIR)/cocos/
mkdir -p $(TARGET_DIR)/var/log/cocos
mkdir -p $(TARGET_DIR)/cocos_init/
$(INSTALL) -D -m 0750 $(@D)/build/cocos-agent $(TARGET_DIR)/bin
endef
define AGENT_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0640 $(@D)/init/systemd/cocos-agent.service $(TARGET_DIR)/usr/lib/systemd/system/cocos-agent.service
$(INSTALL) -D -m 0750 $(@D)/init/systemd/agent_setup.sh $(TARGET_DIR)/cocos_init/agent_setup.sh
$(INSTALL) -D -m 0750 $(@D)/init/systemd/agent_start_script.sh $(TARGET_DIR)/cocos_init/agent_start_script.sh
endef
$(eval $(generic-package))
@@ -0,0 +1,11 @@
config BR2_PACKAGE_ATTESTATION_SERVICE
bool
default y
help
Cocos AI attestation service that generates EAT tokens
for TEE attestation (SNP, TDX, vTPM, Azure).
This service can optionally use the Confidential Containers
attestation-agent as a backend provider via gRPC.
https://github.com/ultravioletrs/cocos
@@ -0,0 +1,34 @@
################################################################################
#
# attestation-service
#
################################################################################
ATTESTATION_SERVICE_VERSION = main
ATTESTATION_SERVICE_SITE = $(call github,ultravioletrs,cocos,$(ATTESTATION_SERVICE_VERSION))
define ATTESTATION_SERVICE_BUILD_CMDS
$(MAKE) -C $(@D) attestation-service
endef
define ATTESTATION_SERVICE_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/build/cocos-attestation-service $(TARGET_DIR)/usr/bin/attestation-service
endef
ifeq ($(BR2_PACKAGE_CC_ATTESTATION_AGENT),y)
define ATTESTATION_SERVICE_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0640 $(@D)/init/systemd/attestation-service.service $(TARGET_DIR)/usr/lib/systemd/system/attestation-service.service
$(INSTALL) -D -m 0750 $(@D)/init/systemd/attestation_setup.sh $(TARGET_DIR)/cocos_init/attestation_setup.sh
# CC attestation agent is already enabled by default
endef
else
define ATTESTATION_SERVICE_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0640 $(@D)/init/systemd/attestation-service.service $(TARGET_DIR)/usr/lib/systemd/system/attestation-service.service
$(INSTALL) -D -m 0750 $(@D)/init/systemd/attestation_setup.sh $(TARGET_DIR)/cocos_init/attestation_setup.sh
# Disable CC attestation agent backend if not selected
sed -i 's/USE_CC_ATTESTATION_AGENT=true/USE_CC_ATTESTATION_AGENT=false/' $(TARGET_DIR)/usr/lib/systemd/system/attestation-service.service
sed -i '/Wants=attestation-agent.service/d' $(TARGET_DIR)/usr/lib/systemd/system/attestation-service.service
endef
endif
$(eval $(generic-package))
@@ -0,0 +1,28 @@
config BR2_PACKAGE_CC_ATTESTATION_AGENT
bool "cc-attestation-agent"
select BR2_PACKAGE_PROTOBUF
select BR2_PACKAGE_OPENSSL
select BR2_PACKAGE_TPM2_TSS
help
Confidential Containers attestation-agent for TEE attestation.
Optional backend for the Cocos AI attestation service that
provides KBS protocol support for remote attestation and
encrypted secret provisioning.
https://github.com/confidential-containers/guest-components
if BR2_PACKAGE_CC_ATTESTATION_AGENT
config BR2_PACKAGE_CC_ATTESTATION_AGENT_KBS_URL
string "Default KBS URL (optional)"
default ""
help
Optional default KBS (Key Broker Service) URL for remote
attestation and secret provisioning.
Leave empty to operate in local attestation mode only.
Example: https://kbs.example.com:8080
endif
@@ -0,0 +1,5 @@
#!/bin/bash
# Setup permissions for attestation socket directory
mkdir -p /run/cocos
chmod 755 /run/cocos
@@ -0,0 +1,37 @@
################################################################################
#
# cc-attestation-agent
#
################################################################################
CC_ATTESTATION_AGENT_VERSION = mvp-runner
CC_ATTESTATION_AGENT_SITE = $(call github,rodneyosodo,guest-components,$(CC_ATTESTATION_AGENT_VERSION))
CC_ATTESTATION_AGENT_LICENSE = Apache-2.0
CC_ATTESTATION_AGENT_LICENSE_FILES = LICENSE
CC_ATTESTATION_AGENT_DEPENDENCIES = host-rustc openssl protobuf tpm2-tss
# Build the attestation-agent from the guest-components repository with gRPC support
define CC_ATTESTATION_AGENT_BUILD_CMDS
cd $(@D)/attestation-agent && \
$(TARGET_MAKE_ENV) \
CARGO_HOME=$(@D)/.cargo \
make ATTESTER=all-attesters ttrpc=false
endef
define CC_ATTESTATION_AGENT_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 \
$(@D)/target/$(RUSTC_TARGET_NAME)/release/attestation-agent \
$(TARGET_DIR)/usr/bin/attestation-agent
endef
define CC_ATTESTATION_AGENT_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0644 \
$(BR2_EXTERNAL_COCOS_PATH)/package/cc-attestation-agent/cc-attestation-agent.service \
$(TARGET_DIR)/usr/lib/systemd/system/attestation-agent.service
$(INSTALL) -D -m 0750 \
$(BR2_EXTERNAL_COCOS_PATH)/package/cc-attestation-agent/cc-attestation-agent-setup.sh \
$(TARGET_DIR)/cocos_init/attestation_setup.sh
endef
$(eval $(generic-package))
@@ -0,0 +1,13 @@
[Unit]
Description=Confidential Containers Attestation Agent (gRPC)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/attestation-agent --attestation_sock 127.0.0.1:50002
Restart=always
RestartSec=5
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,11 @@
config BR2_PACKAGE_COCO_KEYPROVIDER
bool "coco-keyprovider"
depends on BR2_PACKAGE_HOST_RUSTC_ARCH_SUPPORTS
select BR2_PACKAGE_HOST_RUSTC
help
CoCo Keyprovider is a keyprovider tool for generating and
decrypting CoCo-compatible encrypted images. It implements
the ocicrypt keyprovider protocol to decrypt OCI image layers
using the Key Broker Service (KBS).
https://github.com/confidential-containers/guest-components
@@ -0,0 +1,28 @@
#!/bin/sh
set -e
# Read kernel command line
CMDLINE=$(cat /proc/cmdline)
# Extract agent.aa_kbc_params value
# Format: agent.aa_kbc_params=cc_kbc::URL
PARAMS=$(echo "$CMDLINE" | tr ' ' '\n' | grep '^agent.aa_kbc_params=' | cut -d= -f2-)
if [ -n "$PARAMS" ]; then
# Extract URL part (after ::)
KBS_URL="${PARAMS#*::}"
if [ -n "$KBS_URL" ]; then
echo "[coco-keyprovider-setup] Detected KBS URL from kernel cmdline: $KBS_URL"
KBS_ARG="--kbs $KBS_URL"
fi
else
echo "[coco-keyprovider-setup] No agent.aa_kbc_params found in kernel cmdline. Starting without --kbs."
fi
# COCO_KP_SOCKET is set by EnvironmentFile in .service
if [ -z "$COCO_KP_SOCKET" ]; then
COCO_KP_SOCKET="127.0.0.1:50011"
fi
echo "[coco-keyprovider-setup] Starting coco_keyprovider listening on $COCO_KP_SOCKET $KBS_ARG"
exec /usr/local/bin/coco_keyprovider --socket "$COCO_KP_SOCKET" $KBS_ARG
@@ -0,0 +1,3 @@
# CoCo Keyprovider Environment Variables
COCO_KP_SOCKET=127.0.0.1:50011
RUST_LOG=info
@@ -0,0 +1,34 @@
################################################################################
#
# coco-keyprovider
#
################################################################################
COCO_KEYPROVIDER_VERSION = mvp-runner
COCO_KEYPROVIDER_SITE = $(call github,rodneyosodo,guest-components,$(COCO_KEYPROVIDER_VERSION))
COCO_KEYPROVIDER_LICENSE = Apache-2.0
COCO_KEYPROVIDER_LICENSE_FILES = LICENSE
COCO_KEYPROVIDER_DEPENDENCIES = host-rustc
define COCO_KEYPROVIDER_BUILD_CMDS
cd $(@D)/attestation-agent/coco_keyprovider && \
$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) \
CARGO_HOME=$(HOST_DIR)/share/cargo \
cargo build --release --target=$(RUSTC_TARGET_NAME)
endef
define COCO_KEYPROVIDER_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/target/$(RUSTC_TARGET_NAME)/release/coco_keyprovider \
$(TARGET_DIR)/usr/local/bin/coco_keyprovider
$(INSTALL) -D -m 0755 $(BR2_EXTERNAL_COCOS_PATH)/package/coco-keyprovider/coco-keyprovider-setup.sh \
$(TARGET_DIR)/usr/local/bin/coco-keyprovider-setup.sh
$(INSTALL) -D -m 0644 $(BR2_EXTERNAL_COCOS_PATH)/package/coco-keyprovider/coco-keyprovider.service \
$(TARGET_DIR)/etc/systemd/system/coco-keyprovider.service
$(INSTALL) -D -m 0644 $(BR2_EXTERNAL_COCOS_PATH)/package/coco-keyprovider/coco-keyprovider.default \
$(TARGET_DIR)/etc/default/coco-keyprovider
mkdir -p $(TARGET_DIR)/etc
echo '{"key-providers": {"attestation-agent": {"grpc": "127.0.0.1:50011"}}}' > $(TARGET_DIR)/etc/ocicrypt_keyprovider.conf
endef
$(eval $(generic-package))
@@ -0,0 +1,25 @@
[Unit]
Description=CoCo Keyprovider for Confidential Containers
Documentation=https://github.com/confidential-containers/guest-components
After=network-online.target attestation-agent.service
Wants=network-online.target
Requires=attestation-agent.service
[Service]
Type=simple
EnvironmentFile=/etc/default/coco-keyprovider
RuntimeDirectory=coco-keyprovider
ExecStart=/usr/local/bin/coco-keyprovider-setup.sh
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,5 @@
config BR2_PACKAGE_COMPUTATION_RUNNER
bool "computation-runner"
select BR2_PACKAGE_LOG_FORWARDER
help
Cocos AI Computation Runner service.
@@ -0,0 +1,22 @@
################################################################################
#
# computation-runner
#
################################################################################
COMPUTATION_RUNNER_VERSION = main
COMPUTATION_RUNNER_SITE = $(call github,ultravioletrs,cocos,$(COMPUTATION_RUNNER_VERSION))
define COMPUTATION_RUNNER_BUILD_CMDS
$(MAKE) -C $(@D) computation-runner
endef
define COMPUTATION_RUNNER_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0750 $(@D)/build/cocos-computation-runner $(TARGET_DIR)/usr/bin/computation-runner
endef
define COMPUTATION_RUNNER_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0640 $(@D)/init/systemd/computation-runner.service $(TARGET_DIR)/usr/lib/systemd/system/computation-runner.service
endef
$(eval $(generic-package))
+6
View File
@@ -0,0 +1,6 @@
config BR2_PACKAGE_EGRESS_PROXY
bool "egress-proxy"
help
Cocos AI Egress Proxy Service.
https://github.com/ultravioletrs/cocos
@@ -0,0 +1,22 @@
################################################################################
#
# Cocos AI Egress Proxy
#
################################################################################
EGRESS_PROXY_VERSION = main
EGRESS_PROXY_SITE = $(call github,ultravioletrs,cocos,$(EGRESS_PROXY_VERSION))
define EGRESS_PROXY_BUILD_CMDS
$(MAKE) -C $(@D) egress-proxy
endef
define EGRESS_PROXY_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/build/cocos-egress-proxy $(TARGET_DIR)/usr/bin/egress-proxy
endef
define EGRESS_PROXY_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0644 $(@D)/init/systemd/egress-proxy.service $(TARGET_DIR)/usr/lib/systemd/system/egress-proxy.service
endef
$(eval $(generic-package))
+4
View File
@@ -0,0 +1,4 @@
config BR2_PACKAGE_INGRESS_PROXY
bool "ingress-proxy"
help
Cocos Ingress Proxy service.
@@ -0,0 +1,22 @@
################################################################################
#
# ingress-proxy
#
################################################################################
INGRESS_PROXY_VERSION = main
INGRESS_PROXY_SITE = $(call github,ultravioletrs,cocos,$(INGRESS_PROXY_VERSION))
define INGRESS_PROXY_BUILD_CMDS
$(MAKE) -C $(@D) ingress-proxy
endef
define INGRESS_PROXY_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0750 $(@D)/build/cocos-ingress-proxy $(TARGET_DIR)/usr/bin/ingress-proxy
endef
# NOTE: The ingress-proxy is managed per-computation by the agent, not as a standalone
# systemd service. The binary is installed for use by the agent, but no systemd service
# is created.
$(eval $(generic-package))
+4
View File
@@ -0,0 +1,4 @@
config BR2_PACKAGE_LOG_FORWARDER
bool "log-forwarder"
help
Cocos AI Log Forwarder service.
@@ -0,0 +1,22 @@
################################################################################
#
# log-forwarder
#
################################################################################
LOG_FORWARDER_VERSION = main
LOG_FORWARDER_SITE = $(call github,ultravioletrs,cocos,$(LOG_FORWARDER_VERSION))
define LOG_FORWARDER_BUILD_CMDS
$(MAKE) -C $(@D) log-forwarder
endef
define LOG_FORWARDER_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0750 $(@D)/build/cocos-log-forwarder $(TARGET_DIR)/usr/bin/log-forwarder
endef
define LOG_FORWARDER_INSTALL_INIT_SYSTEMD
$(INSTALL) -D -m 0640 $(@D)/init/systemd/log-forwarder.service $(TARGET_DIR)/usr/lib/systemd/system/log-forwarder.service
endef
$(eval $(generic-package))
+6
View File
@@ -0,0 +1,6 @@
config BR2_PACKAGE_WASMEDGE
bool "wasmedge"
default y
help
Wasmedge is a standalone runtime for WebAssembly.
https://wasmedge.org/docs/
+8
View File
@@ -0,0 +1,8 @@
WASMEDGE_DOWNLOAD_URL = https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh
define WASMEDGE_INSTALL_TARGET_CMDS
curl -sSf $(WASMEDGE_DOWNLOAD_URL) | bash -s -- -p $(TARGET_DIR)/usr -v 0.14.1
echo "source /usr/env" >> $(TARGET_DIR)/etc/profile
endef
$(eval $(generic-package))
@@ -0,0 +1,34 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] efi: skip lockdown when built with --disable-shim-lock
When GRUB is built with --disable-shim-lock, grub_shim_lock_verifier_setup()
returns early without registering the shim_lock verifier. However
grub_lockdown() was called unconditionally before that, registering the
lockdown_verifier which marks kernel files as DEFER_AUTH. With no verifier
present to approve them, every kernel load fails with "verification requested
but nobody cares".
Fix by calling grub_shim_lock_verifier_setup() first and only calling
grub_lockdown() if shim_lock is actually active. This preserves full
lockdown behaviour in shim-based chains while allowing direct
OVMF->GRUB->kernel boot with a custom DB key and no shim.
Signed-off-by: Cocos AI <build@cocos.ai>
---
grub-core/kern/efi/init.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/grub-core/kern/efi/init.c b/grub-core/kern/efi/init.c
--- a/grub-core/kern/efi/init.c
+++ b/grub-core/kern/efi/init.c
@@ -122,8 +122,9 @@
*/
if (grub_efi_get_secureboot () == GRUB_EFI_SECUREBOOT_MODE_ENABLED)
{
- grub_lockdown ();
grub_shim_lock_verifier_setup ();
+ if (grub_is_shim_lock_enabled ())
+ grub_lockdown ();
}
grub_efi_system_table->boot_services->set_watchdog_timer (0, 0, 0, NULL);
+1 -1
View File
@@ -10,7 +10,7 @@ HAL uses [Buildroot](https://buildroot.org/)'s [_External Tree_ mechanism](https
git clone git@github.com:ultravioletrs/cocos.git
git clone git@github.com:buildroot/buildroot.git
cd buildroot
git checkout 2025.08-rc3
git checkout 2025.11
make BR2_EXTERNAL=../cocos/hal/linux cocos_defconfig
# Execute 'make menuconfig' only if you want to make additional configuration changes to Buildroot.
make menuconfig
+5 -2
View File
@@ -22,5 +22,8 @@ if [ ! -d "$WORK_DIR" ]; then
mkdir -p $WORK_DIR
fi
# Resize the root file system to 100%
mount -o remount,size=100% /
# RAM-only agent images use tmpfs as the root filesystem
ROOT_FSTYPE=$(awk '$2 == "/" { print $3; exit }' /proc/mounts)
if [ "$ROOT_FSTYPE" = "tmpfs" ]; then
mount -o remount,size=100% /
fi
+2 -2
View File
@@ -1,7 +1,7 @@
[Unit]
Description=Cocos AI agent
After=network.target attestation-service.service log-forwarder.service computation-runner.service egress-proxy.service coco-keyprovider.service
Requires=log-forwarder.service computation-runner.service egress-proxy.service coco-keyprovider.service
After=network.target attestation-service.service log-forwarder.service computation-runner.service egress-proxy.service
Requires=log-forwarder.service computation-runner.service egress-proxy.service
Before=docker.service
[Service]
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/ultravioletrs/cocos/agent/cvms"
)
@@ -8,6 +8,7 @@ package attestation
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
+36 -4
View File
@@ -49,6 +49,12 @@ The service is configured using the environment variables from the following tab
| MANAGER_QEMU_VIRTIO_NET_PCI_ROMFILE | The file path for the ROM image for the virtio-net PCI device. | |
| MANAGER_QEMU_DISK_IMG_KERNEL_FILE | The file path for the kernel image. | img/bzImage |
| MANAGER_QEMU_DISK_IMG_ROOTFS_FILE | The file path for the root filesystem image. | img/rootfs.cpio.gz |
| MANAGER_QEMU_ENABLE_DISK | Whether to attach a writable qcow2 disk to the CVM. | false |
| MANAGER_QEMU_SRC_DISK_FILE | Path to a qcow2 image whose virtual size is used to size the per-VM writable disk. | img/enc_os.qcow2 |
| MANAGER_QEMU_DST_DISK_FILE | Runtime path of the per-VM writable disk created by the manager. | |
| MANAGER_QEMU_DISK_ID | The QEMU drive identifier for the attached disk. | disk0 |
| MANAGER_QEMU_DISK_FORMAT | The format of the attached disk image. | qcow2 |
| MANAGER_QEMU_DISK_SCSI_ID | The SCSI controller identifier used for the attached disk. | scsi0 |
| MANAGER_QEMU_SEV_SNP_ID | The ID for the Secure Encrypted Virtualization (SEV-SNP) device. | sev0 |
| MANAGER_QEMU_SEV_SNP_CBITPOS | The position of the C-bit in the physical address. | 51 |
| MANAGER_QEMU_SEV_SNP_REDUCED_PHYS_BITS | The number of reduced physical address bits for SEV-SNP. | 1 |
@@ -112,6 +118,20 @@ Once the image is built copy the kernel and rootfs image to `cmd/manager/img` fr
Another option is to use release versions of EOS that can be downloaded from the [Cocos GitHub repository](https://github.com/ultravioletrs/cocos/releases).
#### Optional writable disk
If you want the manager to attach a writable disk to each CVM, place a qcow2 reference image at `cmd/manager/img/enc_os.qcow2`, or point `MANAGER_QEMU_SRC_DISK_FILE` to another qcow2 file.
When `MANAGER_QEMU_ENABLE_DISK=true`, the manager:
- reads the virtual size of `MANAGER_QEMU_SRC_DISK_FILE` with `qemu-img info`
- creates a per-VM qcow2 disk under `/tmp/cvmDisk-<uuid>.qcow2`
- sizes the disk to the source image size plus 1 GiB, leaving room for the LUKS header
- attaches the disk through a virtio-scsi controller
- removes the temporary disk again when the VM stops
`MANAGER_QEMU_DST_DISK_FILE` is primarily a runtime value. In the normal manager flow it is populated automatically and usually does not need to be set manually.
#### Test VM creation
```sh
@@ -207,7 +227,7 @@ nc -zv localhost 7020
#### Conclusion
Now you are able to use `Manager` with `Agent`. Namely, `Manager` will create a VM with a separate OVMF variables file on manager `/run` request.
Now you are able to use `Manager` with `Agent`. On each manager `/run` request, the manager creates a VM with a separate OVMF variables file and, when enabled, a per-VM writable qcow2 disk.
### OVMF
@@ -284,6 +304,18 @@ MANAGER_QEMU_OVMF_FILE=<path to OVMF file> \
./build/cocos-manager
```
To enable writable disk support, start manager like this
```sh
MANAGER_GRPC_URL=localhost:7001 \
MANAGER_LOG_LEVEL=debug \
MANAGER_QEMU_ENABLE_DISK=true \
MANAGER_QEMU_SRC_DISK_FILE=<path to reference qcow2 image> \
./build/cocos-manager
```
The reference qcow2 image is used to determine the disk size. The manager creates a fresh writable qcow2 disk for each VM under `/tmp` and deletes it on shutdown.
### Troubleshooting
If the `ps aux | grep qemu-system-x86_64` give you something like this
@@ -294,16 +326,16 @@ darko 13913 0.0 0.0 0 0 pts/2 Z+ 20:17 0:00 [qemu-system-
means that the a QEMU virtual machine that is currently defunct, meaning that it is no longer running. More precisely, the defunct process in the output is also known as a ["zombie" process](https://en.wikipedia.org/wiki/Zombie_process).
You can troubleshoot the VM launch procedure by running directly `qemu-system-x86_64` command. When you run `manager` with `MANAGER_LOG_LEVEL=info` env var set, it prints out the entire command used to launch a VM. The relevant part of the log might look like this
You can troubleshoot the VM launch procedure by running directly `qemu-system-x86_64` command. When you run `manager` with `MANAGER_LOG_LEVEL=info` env var set, it prints out the entire command used to launch a VM. When writable disk support is enabled, the relevant part of the log might look like this
```
{"level":"info","message":"/usr/bin/qemu-system-x86_64 -enable-kvm -machine q35 -cpu EPYC -smp 4,maxcpus=64 -m 4096M,slots=5,maxmem=30G -drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=img/OVMF_VARS.fd -device virtio-scsi-pci,id=scsi,disable-legacy=on,iommu_platform=true -drive file=img/focal-server-cloudimg-amd64.img,if=none,id=disk0,format=qcow2 -device scsi-hd,drive=disk0 -netdev user,id=vmnic,hostfwd=tcp::2222-:22,hostfwd=tcp::9301-:9031,hostfwd=tcp::7020-:7002 -device virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,romfile= -nographic -monitor pty","ts":"2023-08-14T18:29:19.2653908Z"}
{"level":"info","message":"/usr/bin/qemu-system-x86_64 -enable-kvm -machine q35 -cpu EPYC -smp 4,maxcpus=64 -m 4096M,slots=5,maxmem=30G -drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=/tmp/OVMF_VARS-<uuid>.fd -netdev user,id=vmnic-<uuid>,hostfwd=tcp::7020-:7002 -device virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic-<uuid>,addr=0x2,romfile= -drive file=/tmp/cvmDisk-<uuid>.qcow2,if=none,id=disk0,format=qcow2 -device virtio-scsi-pci,id=scsi0,disable-legacy=on,iommu_platform=true -device scsi-hd,drive=disk0,bus=scsi0.0 -kernel img/bzImage -append quiet console=null -initrd img/rootfs.cpio.gz -nographic -monitor pty","ts":"2026-04-27T00:00:00Z"}
```
You can run the command - the value of the `"message"` key - directly in the terminal:
```sh
/usr/bin/qemu-system-x86_64 -enable-kvm -machine q35 -cpu EPYC -smp 4,maxcpus=64 -m 4096M,slots=5,maxmem=30G -drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=img/OVMF_VARS.fd -device virtio-scsi-pci,id=scsi,disable-legacy=on,iommu_platform=true -drive file=img/focal-server-cloudimg-amd64.img,if=none,id=disk0,format=qcow2 -device scsi-hd,drive=disk0 -netdev user,id=vmnic,hostfwd=tcp::2222-:22,hostfwd=tcp::9301-:9031,hostfwd=tcp::7020-:7002 -device virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,romfile= -nographic -monitor pty
/usr/bin/qemu-system-x86_64 -enable-kvm -machine q35 -cpu EPYC -smp 4,maxcpus=64 -m 4096M,slots=5,maxmem=30G -drive if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=/tmp/OVMF_VARS-<uuid>.fd -netdev user,id=vmnic-<uuid>,hostfwd=tcp::7020-:7002 -device virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic-<uuid>,addr=0x2,romfile= -drive file=/tmp/cvmDisk-<uuid>.qcow2,if=none,id=disk0,format=qcow2 -device virtio-scsi-pci,id=scsi0,disable-legacy=on,iommu_platform=true -device scsi-hd,drive=disk0,bus=scsi0.0 -kernel img/bzImage -append "quiet console=null" -initrd img/rootfs.cpio.gz -nographic -monitor pty
```
and look for the possible problems. This problems can usually be solved by using the adequate env var assignments. Look in the `manager/qemu/config.go` file to see the recognized env vars. Don't forget to prepend `MANAGER_QEMU_` to the name of the env vars.
+2 -2
View File
@@ -6,14 +6,14 @@ package http
import (
"net/http"
"github.com/absmach/supermq"
"github.com/absmach/magistrala"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(r *chi.Mux, svcName, instanceID string) http.Handler {
r.Get("/health", supermq.Health(svcName, instanceID))
r.Get("/health", magistrala.Health(svcName, instanceID))
r.Handle("/metrics", promhttp.Handler())
return r
+92 -6
View File
@@ -4,6 +4,7 @@ package qemu
import (
"fmt"
"strings"
"github.com/caarlos0/env/v10"
)
@@ -48,7 +49,7 @@ type VirtioNetPciConfig struct {
ROMFile string `env:"VIRTIO_NET_PCI_ROMFILE"`
}
type DiskImgConfig struct {
type KernelConfig struct {
KernelFile string `env:"DISK_IMG_KERNEL_FILE" envDefault:"img/bzImage"`
RootFsFile string `env:"DISK_IMG_ROOTFS_FILE" envDefault:"img/rootfs.cpio.gz"`
}
@@ -72,9 +73,26 @@ type IGVMConfig struct {
File string `env:"IGVM_FILE" envDefault:"/root/coconut-qemu.igvm"`
}
type DiskConfig struct {
SrcFile string `env:"SRC_DISK_FILE" envDefault:"img/enc_os.qcow2"`
DstFile string `env:"DST_DISK_FILE" envDefault:""`
ID string `env:"DISK_ID" envDefault:"disk0"`
Format string `env:"DISK_FORMAT" envDefault:"qcow2"`
SCSIID string `env:"DISK_SCSI_ID" envDefault:"scsi0"`
}
type GPUConfig struct {
EnableGPU bool
GPUBDF string `env:"GPU_BDF" envDefault:""`
PCIeRootPort string `env:"GPU_PCIE_ROOT_PORT" envDefault:"pci.1"`
PCIeBus string `env:"GPU_PCIE_BUS" envDefault:"pcie.0"`
FWCfgPciMmio string `env:"GPU_FW_CFG_MMIO_MB" envDefault:"262144"`
}
type Config struct {
EnableSEVSNP bool
EnableTDX bool
EnableDisk bool `env:"ENABLE_DISK" envDefault:"false"`
QemuBinPath string `env:"BIN_PATH" envDefault:"qemu-system-x86_64"`
UseSudo bool `env:"USE_SUDO" envDefault:"false"`
@@ -96,8 +114,11 @@ type Config struct {
NetDevConfig
VirtioNetPciConfig
// disk
DiskImgConfig
// disk config
DiskConfig
// kernel and initramfs
KernelConfig
// SEV-SNP
SEVSNPConfig
@@ -108,6 +129,9 @@ type Config struct {
// vTPM
IGVMConfig
// GPU passthrough
GPUConfig
// display
NoGraphic bool `env:"NO_GRAPHIC" envDefault:"true"`
Monitor string `env:"MONITOR" envDefault:"pty"`
@@ -123,6 +147,25 @@ type Config struct {
EnvMount string `env:"ENV_MOUNT" envDefault:""`
}
func (config Config) ValidateBootConfig() error {
if config.EnableDisk {
if strings.TrimSpace(config.DiskConfig.DstFile) == "" {
return fmt.Errorf("disk boot enabled but destination disk image is not set")
}
return nil
}
if strings.TrimSpace(config.KernelConfig.KernelFile) == "" {
return fmt.Errorf("kernel boot enabled but kernel image is not set")
}
if strings.TrimSpace(config.KernelConfig.RootFsFile) == "" {
return fmt.Errorf("kernel boot enabled but initramfs image is not set")
}
return nil
}
func (config Config) ConstructQemuArgs() []string {
args := []string{}
@@ -179,6 +222,39 @@ func (config Config) ConstructQemuArgs() []string {
config.VirtioNetPciConfig.Addr,
config.VirtioNetPciConfig.ROMFile))
if config.EnableDisk {
// disk image
args = append(args, "-drive",
fmt.Sprintf("file=%s,if=none,id=%s,format=%s",
config.DiskConfig.DstFile,
config.DiskConfig.ID,
config.DiskConfig.Format))
args = append(args, "-device",
fmt.Sprintf("virtio-scsi-pci,id=%s,disable-legacy=on,iommu_platform=true",
config.DiskConfig.SCSIID))
args = append(args, "-device",
fmt.Sprintf("scsi-hd,drive=%s,bus=%s.0",
config.DiskConfig.ID,
config.DiskConfig.SCSIID))
}
// GPU passthrough via VFIO
if config.GPUConfig.EnableGPU {
args = append(args, "-device",
fmt.Sprintf("pcie-root-port,id=%s,bus=%s",
config.GPUConfig.PCIeRootPort,
config.GPUConfig.PCIeBus))
args = append(args, "-device",
fmt.Sprintf("vfio-pci,host=%s,bus=%s",
config.GPUConfig.GPUBDF,
config.GPUConfig.PCIeRootPort))
args = append(args, "-fw_cfg",
fmt.Sprintf("name=opt/ovmf/X-PciMmio64Mb,string=%s",
config.GPUConfig.FWCfgPciMmio))
}
// SEV-SNP
if config.EnableSEVSNP {
sevSnpType := "sev-snp-guest"
@@ -233,9 +309,11 @@ func (config Config) ConstructQemuArgs() []string {
args = append(args, "-nodefaults")
}
args = append(args, "-kernel", config.DiskImgConfig.KernelFile)
args = append(args, "-append", config.KernelCommandLine)
args = append(args, "-initrd", config.DiskImgConfig.RootFsFile)
if !config.EnableDisk {
args = append(args, "-kernel", config.KernelConfig.KernelFile)
args = append(args, "-append", config.KernelCommandLine)
args = append(args, "-initrd", config.KernelConfig.RootFsFile)
}
// display
if config.NoGraphic {
@@ -267,5 +345,13 @@ func NewConfig() (*Config, error) {
cfg.EnableSEVSNP = SEVSNPEnabledOnHost()
cfg.EnableTDX = TDXEnabledOnHost()
bdf, detected := GPUPassthroughAvailable()
if cfg.GPUConfig.GPUBDF != "" {
cfg.GPUConfig.EnableGPU = true
} else if detected {
cfg.GPUConfig.EnableGPU = true
cfg.GPUConfig.GPUBDF = bdf
}
return &cfg, nil
}
+226 -2
View File
@@ -51,7 +51,7 @@ func TestConstructQemuArgs(t *testing.T) {
IOMMUPlatform: true,
Addr: "0x2",
},
DiskImgConfig: DiskImgConfig{
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
@@ -115,7 +115,7 @@ func TestConstructQemuArgs(t *testing.T) {
IOMMUPlatform: true,
Addr: "0x2",
},
DiskImgConfig: DiskImgConfig{
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
@@ -151,6 +151,79 @@ func TestConstructQemuArgs(t *testing.T) {
"-monitor", "pty",
},
},
{
name: "GPU passthrough configuration",
config: Config{
QemuBinPath: "qemu-system-x86_64",
EnableKVM: true,
Machine: "q35",
CPU: "EPYC",
SMPCount: 4,
MaxCPUs: 64,
MemID: "ram1",
MemoryConfig: MemoryConfig{
Size: "2048M",
Slots: 5,
Max: "30G",
},
OVMFCodeConfig: OVMFCodeConfig{
If: "pflash",
Format: "raw",
Unit: 0,
File: "/usr/share/OVMF/OVMF_CODE.fd",
ReadOnly: "on",
},
OVMFVarsConfig: OVMFVarsConfig{
If: "pflash",
Format: "raw",
Unit: 1,
File: "/usr/share/OVMF/OVMF_VARS.fd",
},
NetDevConfig: NetDevConfig{
ID: "vmnic",
HostFwdAgent: 7020,
GuestFwdAgent: 7002,
},
VirtioNetPciConfig: VirtioNetPciConfig{
DisableLegacy: "on",
IOMMUPlatform: true,
Addr: "0x2",
},
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
GPUConfig: GPUConfig{
EnableGPU: true,
GPUBDF: "0000:02:00.0",
PCIeRootPort: "pci.1",
PCIeBus: "pcie.0",
FWCfgPciMmio: "262144",
},
KernelCommandLine: "quiet console=null",
NoGraphic: true,
Monitor: "pty",
},
expected: []string{
"-enable-kvm",
"-machine", "q35",
"-cpu", "EPYC",
"-smp", "4,maxcpus=64",
"-m", "2048M,slots=5,maxmem=30G",
"-drive", "if=pflash,format=raw,unit=0,file=/usr/share/OVMF/OVMF_CODE.fd,readonly=on",
"-drive", "if=pflash,format=raw,unit=1,file=/usr/share/OVMF/OVMF_VARS.fd",
"-netdev", "user,id=vmnic,hostfwd=tcp::7020-:7002",
"-device", "virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,addr=0x2,romfile=",
"-device", "pcie-root-port,id=pci.1,bus=pcie.0",
"-device", "vfio-pci,host=0000:02:00.0,bus=pci.1",
"-fw_cfg", "name=opt/ovmf/X-PciMmio64Mb,string=262144",
"-kernel", "img/bzImage",
"-append", "quiet console=null",
"-initrd", "img/rootfs.cpio.gz",
"-nographic",
"-monitor", "pty",
},
},
}
for _, tt := range tests {
@@ -194,3 +267,154 @@ func TestConstructQemuArgs_HostData(t *testing.T) {
t.Errorf("ConstructQemuArgs() did not contain expected SEV-SNP configuration with host data")
}
}
func TestConstructQemuArgs_TDX(t *testing.T) {
config := Config{
EnableKVM: true,
EnableTDX: true,
Machine: "q35",
CPU: "EPYC",
SMPCount: 4,
MaxCPUs: 64,
MemID: "ram1",
MemoryConfig: MemoryConfig{
Size: "4096M",
Slots: 8,
Max: "64G",
},
NetDevConfig: NetDevConfig{
ID: "vmnic",
HostFwdAgent: 7020,
GuestFwdAgent: 7002,
},
VirtioNetPciConfig: VirtioNetPciConfig{
DisableLegacy: "on",
IOMMUPlatform: true,
Addr: "0x2",
},
TDXConfig: TDXConfig{
ID: "tdx0",
QuoteGenerationPort: 4050,
OVMF: "/usr/share/ovmf/OVMF.fd",
},
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
KernelCommandLine: "quiet console=null",
NoGraphic: true,
Monitor: "pty",
}
expected := []string{
"-enable-kvm",
"-machine", "q35",
"-cpu", "EPYC",
"-smp", "4,maxcpus=64",
"-m", "4096M,slots=8,maxmem=64G",
"-netdev", "user,id=vmnic,hostfwd=tcp::7020-:7002",
"-device", "virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,addr=0x2,romfile=",
"-object", "{\"qom-type\":\"tdx-guest\",\"id\":\"tdx0\",\"quote-generation-socket\":{\"type\": \"vsock\", \"cid\":\"2\",\"port\":\"4050\"}}",
"-machine", "confidential-guest-support=tdx0,memory-backend=ram1,hpet=off",
"-object", "memory-backend-memfd,id=ram1,size=4096M,share=true,prealloc=false",
"-bios", "/usr/share/ovmf/OVMF.fd",
"-nodefaults",
"-kernel", "img/bzImage",
"-append", "quiet console=null",
"-initrd", "img/rootfs.cpio.gz",
"-nographic",
"-monitor", "pty",
}
result := config.ConstructQemuArgs()
if !reflect.DeepEqual(result, expected) {
t.Errorf("ConstructQemuArgs() = %v, want %v", result, expected)
}
}
func TestConstructQemuArgs_DiskBootSkipsKernelAndInitrd(t *testing.T) {
config := Config{
EnableKVM: true,
EnableDisk: true,
Machine: "q35",
CPU: "EPYC",
SMPCount: 4,
MaxCPUs: 64,
MemID: "ram1",
MemoryConfig: MemoryConfig{
Size: "2048M",
Slots: 5,
Max: "30G",
},
NetDevConfig: NetDevConfig{
ID: "vmnic",
HostFwdAgent: 7020,
GuestFwdAgent: 7002,
},
VirtioNetPciConfig: VirtioNetPciConfig{
DisableLegacy: "on",
IOMMUPlatform: true,
Addr: "0x2",
},
DiskConfig: DiskConfig{
DstFile: "img/disk.img",
ID: "disk0",
Format: "qcow2",
SCSIID: "scsi0",
},
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
NoGraphic: true,
Monitor: "pty",
}
result := config.ConstructQemuArgs()
for _, forbidden := range []string{"-kernel", "-append", "-initrd"} {
for _, arg := range result {
if arg == forbidden {
t.Fatalf("ConstructQemuArgs() unexpectedly contained %s during disk boot: %v", forbidden, result)
}
}
}
}
func TestConstructQemuArgs_EnableDisk(t *testing.T) {
config := Config{
EnableDisk: true,
DiskConfig: DiskConfig{
SrcFile: "img/enc_os.qcow2",
DstFile: "img/enc_os_dst.qcow2",
ID: "disk0",
Format: "qcow2",
SCSIID: "scsi0",
},
}
result := config.ConstructQemuArgs()
expected := []string{
"-drive", "file=img/enc_os_dst.qcow2,if=none,id=disk0,format=qcow2",
"-device", "virtio-scsi-pci,id=scsi0,disable-legacy=on,iommu_platform=true",
"-device", "scsi-hd,drive=disk0,bus=scsi0.0",
}
var found []bool = make([]bool, len(expected))
for i, arg := range result {
for j := 0; j < len(expected); j += 2 {
if arg == expected[j] && i+1 < len(result) && result[i+1] == expected[j+1] {
found[j] = true
found[j+1] = true
break
}
}
}
for j, f := range found {
if !f {
t.Errorf("ConstructQemuArgs() did not contain expected disk configuration: %s", expected[j])
}
}
}
+127 -6
View File
@@ -3,10 +3,12 @@
package qemu
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
@@ -18,12 +20,15 @@ import (
)
const (
firmwareVars = "OVMF_VARS"
KernelFile = "bzImage"
rootfsFile = "rootfs.cpio"
tmpDir = "/tmp"
interval = 5 * time.Second
shutdownTimeout = 30 * time.Second
firmwareVars = "OVMF_VARS"
KernelFile = "bzImage"
rootfsFile = "rootfs.cpio"
tmpDir = "/tmp"
diskDstName = "cvmDisk"
interval = 5 * time.Second
shutdownTimeout = 30 * time.Second
encryptedPartitionSizeDeltaGB = 1
sourceDiskFormat = "qcow2"
)
type VMInfo struct {
@@ -39,6 +44,10 @@ type qemuVM struct {
vm.StateMachine
}
type qemuInfo struct {
VirtualSize int64 `json:"virtual-size"`
}
func NewVM(config any, cvmId string, logger *slog.Logger) vm.VM {
return &qemuVM{
vmi: config.(VMInfo),
@@ -75,6 +84,44 @@ func (v *qemuVM) Start() (err error) {
v.vmi.Config.OVMFVarsConfig.File = dstFile
}
if v.vmi.Config.EnableDisk {
srcDiskFile, err := filepath.Abs(v.vmi.Config.SrcFile)
if err != nil {
return err
}
sizeGB, err := GetVirtualSizeGB(srcDiskFile)
if err != nil {
return err
}
dstDiskFile := fmt.Sprintf("%s/%s-%s.%s", tmpDir, diskDstName, id, v.vmi.Config.DiskConfig.Format)
sizeArg := fmt.Sprintf("%dG", sizeGB+encryptedPartitionSizeDeltaGB)
cmd := exec.Command(
"qemu-img",
"convert",
"-f", sourceDiskFormat,
"-O", v.vmi.Config.DiskConfig.Format,
srcDiskFile,
dstDiskFile,
)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("qemu-img convert failed: %w: %s", err, string(out))
}
cmd = exec.Command(
"qemu-img",
"resize",
dstDiskFile,
sizeArg,
)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("qemu-img resize failed: %w: %s", err, string(out))
}
v.vmi.Config.DstFile = dstDiskFile
}
exe, args, err := v.executableAndArgs()
if err != nil {
return err
@@ -111,6 +158,14 @@ func (v *qemuVM) Stop() error {
}
}
if v.vmi.Config.EnableDisk {
if v.vmi.Config.DstFile != "" {
if err := os.RemoveAll(v.vmi.Config.DstFile); err != nil {
return fmt.Errorf("failed to remove disk file: %v", err)
}
}
}
done := make(chan error, 1)
go func() {
_, err := v.cmd.Process.Wait()
@@ -156,6 +211,10 @@ func (v *qemuVM) executableAndArgs() (string, []string, error) {
return "", nil, err
}
if err := v.vmi.Config.ValidateBootConfig(); err != nil {
return "", nil, err
}
args := v.vmi.Config.ConstructQemuArgs()
if v.vmi.Config.UseSudo {
@@ -231,3 +290,65 @@ func TDXEnabledOnHost() bool {
return TDXEnabled(string(cpuinfo), string(kernelParam))
}
func GetVirtualSizeBytes(path string) (int64, error) {
cmd := exec.Command("qemu-img", "info", "--output=json", path)
out, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("qemu-img info failed: %w", err)
}
var info qemuInfo
if err := json.Unmarshal(out, &info); err != nil {
return 0, fmt.Errorf("failed to parse qemu-img JSON: %w", err)
}
if info.VirtualSize <= 0 {
return 0, fmt.Errorf("invalid virtual size: %d", info.VirtualSize)
}
return info.VirtualSize, nil
}
func GetVirtualSizeGB(path string) (int, error) {
bytes, err := GetVirtualSizeBytes(path)
if err != nil {
return 0, err
}
gb := (bytes + (1<<30 - 1)) >> 30
return int(gb), nil
}
// GPUPassthroughAvailable scans for NVIDIA GPU devices bound to the vfio-pci
// driver and returns the BDF of the first one found.
func GPUPassthroughAvailable() (string, bool) {
const vfioPCIPath = "/sys/bus/pci/drivers/vfio-pci"
entries, err := os.ReadDir(vfioPCIPath)
if err != nil {
return "", false
}
for _, entry := range entries {
bdf := entry.Name()
if !strings.Contains(bdf, ":") {
continue
}
vendor, err := os.ReadFile(fmt.Sprintf("/sys/bus/pci/devices/%s/vendor", bdf))
if err != nil || strings.TrimSpace(string(vendor)) != "0x10de" {
continue
}
class, err := os.ReadFile(fmt.Sprintf("/sys/bus/pci/devices/%s/class", bdf))
if err != nil {
continue
}
classStr := strings.TrimSpace(string(class))
// 0x0302xx = 3D Controller (e.g. H100), 0x0300xx = VGA Compatible Controller
if strings.HasPrefix(classStr, "0x0302") || strings.HasPrefix(classStr, "0x0300") {
return bdf, true
}
}
return "", false
}
+358 -3
View File
@@ -3,9 +3,12 @@
package qemu
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -15,6 +18,31 @@ import (
const testComputationID = "test-computation"
func cleanupStrayQcow2(t *testing.T) {
t.Helper()
wd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
t.Cleanup(func() {
_ = os.Remove(filepath.Join(wd, "qcow2"))
})
}
func requireTempFile(t *testing.T, path string) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatalf("failed to create temp file %s: %v", path, err)
}
if err := f.Close(); err != nil {
t.Fatalf("failed to close temp file %s: %v", path, err)
}
}
func TestNewVM(t *testing.T) {
config := VMInfo{Config: Config{}}
@@ -35,6 +63,10 @@ func TestStart(t *testing.T) {
File: tmpFile.Name(),
},
QemuBinPath: "echo",
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
}}
vm := NewVM(config, testComputationID, slog.Default()).(*qemuVM)
@@ -58,6 +90,10 @@ func TestStartSudo(t *testing.T) {
},
QemuBinPath: "echo",
UseSudo: true,
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
}}
vm := NewVM(config, testComputationID, slog.Default()).(*qemuVM)
@@ -69,6 +105,136 @@ func TestStartSudo(t *testing.T) {
_ = vm.Stop()
}
func TestStart_EnableDisk(t *testing.T) {
cleanupStrayQcow2(t)
toolsDir := t.TempDir()
convertLogFile := filepath.Join(toolsDir, "qemu-img-convert.log")
resizeLogFile := filepath.Join(toolsDir, "qemu-img-resize.log")
srcDiskFile := filepath.Join(toolsDir, "enc_os.qcow2")
requireTempFile(t, srcDiskFile)
writeFakeExecutable(t, toolsDir, "qemu-img", fmt.Sprintf(`#!/bin/sh
case "$1" in
info)
printf '%%s' '{"virtual-size":2147483648}'
;;
convert)
printf '%%s\n' "$@" > %q
dst="$7"
: > "$dst"
;;
resize)
printf '%%s\n' "$@" > %q
;;
*)
echo "unexpected subcommand: $1" >&2
exit 2
;;
esac
`, convertLogFile, resizeLogFile))
writeFakeExecutable(t, toolsDir, "fake-qemu", `#!/bin/sh
trap 'exit 0' TERM INT
while :; do
sleep 1
done
`)
prependPath(t, toolsDir)
config := VMInfo{Config: Config{
EnableTDX: true,
EnableDisk: true,
QemuBinPath: "fake-qemu",
DiskConfig: DiskConfig{
SrcFile: srcDiskFile,
ID: "disk0",
Format: "qcow2",
SCSIID: "scsi0",
},
}}
vm := NewVM(config, testComputationID, slog.Default()).(*qemuVM)
err := vm.Start()
assert.NoError(t, err)
assert.NotNil(t, vm.cmd)
assert.Contains(t, vm.vmi.Config.DstFile, filepath.Join(tmpDir, diskDstName))
_, err = os.Stat(vm.vmi.Config.DstFile)
assert.NoError(t, err)
loggedArgs, err := os.ReadFile(convertLogFile)
assert.NoError(t, err)
assert.Equal(t, []string{
"convert",
"-f",
"qcow2",
"-O",
"qcow2",
srcDiskFile,
vm.vmi.Config.DstFile,
}, strings.Fields(string(loggedArgs)))
loggedArgs, err = os.ReadFile(resizeLogFile)
assert.NoError(t, err)
assert.Equal(t, []string{
"resize",
vm.vmi.Config.DstFile,
"3G",
}, strings.Fields(string(loggedArgs)))
err = vm.Stop()
assert.NoError(t, err)
_, err = os.Stat(vm.vmi.Config.DstFile)
assert.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestStart_EnableDiskCreateError(t *testing.T) {
cleanupStrayQcow2(t)
toolsDir := t.TempDir()
srcDiskFile := filepath.Join(toolsDir, "enc_os.qcow2")
requireTempFile(t, srcDiskFile)
writeFakeExecutable(t, toolsDir, "qemu-img", `#!/bin/sh
case "$1" in
info)
printf '%s' '{"virtual-size":2147483648}'
;;
convert)
echo 'disk create failed' >&2
exit 1
;;
resize)
exit 0
;;
*)
echo "unexpected subcommand: $1" >&2
exit 2
;;
esac
`)
prependPath(t, toolsDir)
config := VMInfo{Config: Config{
EnableTDX: true,
EnableDisk: true,
QemuBinPath: "fake-qemu",
DiskConfig: DiskConfig{
SrcFile: srcDiskFile,
},
}}
vm := NewVM(config, testComputationID, slog.Default()).(*qemuVM)
err := vm.Start()
assert.Error(t, err)
assert.ErrorContains(t, err, "qemu-img convert failed")
assert.ErrorContains(t, err, "disk create failed")
assert.Nil(t, vm.cmd)
}
func TestStop(t *testing.T) {
t.Run("success", func(t *testing.T) {
cmd := exec.Command("echo", "test")
@@ -102,6 +268,42 @@ func TestStop(t *testing.T) {
StateMachine: sm,
}
err = vm.Stop()
assert.NoError(t, err)
})
t.Run("disk enable", func(t *testing.T) {
dir := t.TempDir()
dst := filepath.Join(dir, "disk.qcow2")
f, err := os.Create(dst)
if err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
cmd := exec.Command("echo", "test")
err = cmd.Start()
assert.NoError(t, err)
sm := new(mocks.StateMachine)
sm.On("Transition", pkgmanager.StopComputationRun).Return(nil)
vm := &qemuVM{
vmi: VMInfo{
Config: Config{
EnableDisk: true,
DiskConfig: DiskConfig{
DstFile: dst,
},
},
},
cmd: &exec.Cmd{
Process: cmd.Process,
},
StateMachine: sm,
}
err = vm.Stop()
assert.NoError(t, err)
})
@@ -110,7 +312,13 @@ func TestStop(t *testing.T) {
func TestSetProcess(t *testing.T) {
vm := &qemuVM{
vmi: VMInfo{
Config: Config{QemuBinPath: "echo"}, // Use 'echo' as a dummy QEMU binary
Config: Config{
QemuBinPath: "echo", // Use 'echo' as a dummy QEMU binary
KernelConfig: KernelConfig{
KernelFile: "img/bzImage",
RootFsFile: "img/rootfs.cpio.gz",
},
},
},
}
@@ -175,9 +383,156 @@ func TestTDXEnabled(t *testing.T) {
}
func TestSEVSNPEnabledOnHost(t *testing.T) {
assert.False(t, SEVSNPEnabledOnHost())
cpuinfo, cpuErr := os.ReadFile("/proc/cpuinfo")
kernelParam, kernelErr := os.ReadFile("/sys/module/kvm_amd/parameters/sev_snp")
expected := false
if cpuErr == nil && kernelErr == nil {
expected = SEVSNPEnabled(string(cpuinfo), string(kernelParam))
}
assert.Equal(t, expected, SEVSNPEnabledOnHost())
}
func TestTDXEnabledOnHost(t *testing.T) {
assert.False(t, TDXEnabledOnHost())
cpuinfo, cpuErr := os.ReadFile("/proc/cpuinfo")
kernelParam, kernelErr := os.ReadFile("/sys/module/kvm_intel/parameters/tdx")
expected := false
if cpuErr == nil && kernelErr == nil {
expected = TDXEnabled(string(cpuinfo), string(kernelParam))
}
assert.Equal(t, expected, TDXEnabledOnHost())
}
func TestGetVirtualSizeBytes_Success(t *testing.T) {
cleanup := writeFakeQemuImg(t, `{"virtual-size":2147483648}`, 0) // 2 GiB
defer cleanup()
got, err := GetVirtualSizeBytes("whatever.qcow2")
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if got != 2147483648 {
t.Fatalf("expected 2147483648, got %d", got)
}
}
func TestGetVirtualSizeBytes_CommandFailure(t *testing.T) {
cleanup := writeFakeQemuImg(t, `{"virtual-size":2147483648}`, 1) // non-zero exit
defer cleanup()
_, err := GetVirtualSizeBytes("whatever.qcow2")
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), "qemu-img info failed") {
t.Fatalf("expected wrapped error to contain %q, got %q", "qemu-img info failed", err.Error())
}
}
func TestGetVirtualSizeBytes_InvalidJSON(t *testing.T) {
cleanup := writeFakeQemuImg(t, `not-json`, 0)
defer cleanup()
_, err := GetVirtualSizeBytes("whatever.qcow2")
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), "failed to parse qemu-img JSON") {
t.Fatalf("expected error to contain %q, got %q", "failed to parse qemu-img JSON", err.Error())
}
}
func TestGetVirtualSizeBytes_InvalidVirtualSize(t *testing.T) {
cleanup := writeFakeQemuImg(t, `{"virtual-size":0}`, 0)
defer cleanup()
_, err := GetVirtualSizeBytes("whatever.qcow2")
if err == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(err.Error(), "invalid virtual size") {
t.Fatalf("expected error to contain %q, got %q", "invalid virtual size", err.Error())
}
}
func TestGetVirtualSizeGB_RoundsUp(t *testing.T) {
tests := []struct {
name string
virtualSz int64
wantGB int
}{
{"exact_1GiB", 1 << 30, 1},
{"one_byte_over", (1 << 30) + 1, 2},
{"just_under_2GiB", (2 << 30) - 1, 2},
{"exact_2GiB", 2 << 30, 2},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cleanup := writeFakeQemuImg(t, fmt.Sprintf(`{"virtual-size":%d}`, tc.virtualSz), 0)
defer cleanup()
got, err := GetVirtualSizeGB("whatever.qcow2")
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if got != tc.wantGB {
t.Fatalf("expected %d, got %d", tc.wantGB, got)
}
})
}
}
func writeFakeQemuImg(t *testing.T, stdout string, exitCode int) func() {
dir := t.TempDir()
fake := filepath.Join(dir, "qemu-img")
script := fmt.Sprintf(`#!/bin/sh
# Minimal fake for: qemu-img info --output=json <path>
if [ "$1" != "info" ]; then
echo "unexpected subcommand: $1" >&2
exit 2
fi
# always print provided stdout, even if empty
printf '%s' %q
exit %d
`, stdout, stdout, exitCode)
if err := os.WriteFile(fake, []byte(script), 0o755); err != nil {
t.Fatalf("failed to write fake qemu-img: %v", err)
}
oldPath := os.Getenv("PATH")
if err := os.Setenv("PATH", dir+string(os.PathListSeparator)+oldPath); err != nil {
t.Fatalf("failed to set PATH: %v", err)
}
return func() {
_ = os.Setenv("PATH", oldPath)
}
}
func writeFakeExecutable(t *testing.T, dir, name, script string) {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
t.Fatalf("failed to write fake executable %q: %v", name, err)
}
}
func prependPath(t *testing.T, dir string) {
t.Helper()
oldPath := os.Getenv("PATH")
if err := os.Setenv("PATH", dir+string(os.PathListSeparator)+oldPath); err != nil {
t.Fatalf("failed to set PATH: %v", err)
}
t.Cleanup(func() {
_ = os.Setenv("PATH", oldPath)
})
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"syscall"
"time"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/uuid"
"github.com/ultravioletrs/cocos/manager/qemu"
"github.com/ultravioletrs/cocos/manager/vm"
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"path/filepath"
"testing"
mglog "github.com/absmach/supermq/logger"
"github.com/absmach/supermq/pkg/errors"
mglog "github.com/absmach/magistrala/logger"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+2 -2
View File
@@ -20,7 +20,7 @@ import (
attestation "github.com/ultravioletrs/cocos/pkg/atls/eaattestation"
)
func selfSignedCert(t *testing.T) tls.Certificate {
func selfSignedCert(t testing.TB) tls.Certificate {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
@@ -42,7 +42,7 @@ func selfSignedCert(t *testing.T) tls.Certificate {
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv}
}
func tlsPair(t *testing.T, cert tls.Certificate) (srv, cli *tls.Conn) {
func tlsPair(t testing.TB, cert tls.Certificate) (srv, cli *tls.Conn) {
t.Helper()
srvConf := &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13}
cliConf := &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13}
+81 -5
View File
@@ -4,24 +4,39 @@
package atls
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time"
eaattestation "github.com/ultravioletrs/cocos/pkg/atls/eaattestation"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/azure"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/ultravioletrs/cocos/pkg/attestation/gpu"
"github.com/ultravioletrs/cocos/pkg/attestation/tdx"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/veraison/corim/corim"
)
type policyEvidenceVerifier struct {
policyPath string
policyPath string
loadManifest func(string) (*corim.UnsignedCorim, error)
rootVerifier func(cocosattestation.PlatformType) (cocosattestation.Verifier, error)
newGPUVerifier func() (cocosattestation.Verifier, error)
}
func NewEvidenceVerifier(policyPath string) eaattestation.EvidenceVerifier {
return &policyEvidenceVerifier{policyPath: policyPath}
return &policyEvidenceVerifier{
policyPath: policyPath,
loadManifest: loadCoRIM,
rootVerifier: platformVerifier,
newGPUVerifier: defaultGPUVerifier,
}
}
func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
@@ -32,15 +47,25 @@ func (v *policyEvidenceVerifier) VerifyEvidence(evidence []byte) error {
if err != nil {
return fmt.Errorf("atls: failed to decode EAT evidence: %w", err)
}
manifest, err := loadCoRIM(v.policyPath)
manifest, err := v.loadManifest(v.policyPath)
if err != nil {
return err
}
verifier, err := platformVerifier(platformTypeFromClaims(claims.PlatformType))
verifier, err := v.rootVerifier(platformTypeFromClaims(claims.PlatformType))
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.RawReport, manifest)
if err := verifier.VerifyWithCoRIM(claims.RawReport, manifest); err != nil {
return err
}
if claims.GPUExtensions != nil {
if err := v.verifyGPUEvidence(claims, manifest); err != nil {
return err
}
}
return nil
}
func loadCoRIM(path string) (*corim.UnsignedCorim, error) {
@@ -90,3 +115,54 @@ func platformVerifier(platformType cocosattestation.PlatformType) (cocosattestat
return nil, fmt.Errorf("atls: unsupported platform type: %d", platformType)
}
}
func defaultGPUVerifier() (cocosattestation.Verifier, error) {
timeout := 30 * time.Second
if rawTimeout := os.Getenv("ATLS_GPU_VERIFIER_TIMEOUT"); rawTimeout != "" {
parsed, err := time.ParseDuration(rawTimeout)
if err != nil {
return nil, fmt.Errorf("atls: invalid ATLS_GPU_VERIFIER_TIMEOUT: %w", err)
}
timeout = parsed
}
binaryPath := os.Getenv("ATLS_GPU_VERIFIER_PATH")
if binaryPath == "" {
binaryPath = os.Getenv("ATTESTATION_GPU_HELPER_PATH")
}
return gpu.NewVerifier(binaryPath, timeout)
}
func (v *policyEvidenceVerifier) verifyGPUEvidence(claims *eat.EATClaims, manifest *corim.UnsignedCorim) error {
if len(claims.GPUExtensions.EvidenceJSON) == 0 {
return fmt.Errorf("atls: gpu evidence is empty")
}
expectedNonce := sha256.Sum256(append(append([]byte(nil), claims.Nonce...), []byte(":gpu")...))
if !bytes.Equal(claims.GPUExtensions.Nonce, expectedNonce[:]) {
return fmt.Errorf("atls: gpu nonce binding mismatch")
}
// Guard against replay: a stale self-consistent EvidenceJSON blob can be
// paired with a rewritten outer Nonce unless the inner nonce is also checked.
var envelopes []struct {
Nonce string `json:"nonce"`
}
if err := json.Unmarshal(claims.GPUExtensions.EvidenceJSON, &envelopes); err != nil {
return fmt.Errorf("atls: failed to parse GPU evidence JSON: %w", err)
}
if len(envelopes) == 0 || strings.TrimSpace(envelopes[0].Nonce) == "" {
return fmt.Errorf("atls: GPU evidence JSON missing nonce")
}
if envelopes[0].Nonce != hex.EncodeToString(expectedNonce[:]) {
return fmt.Errorf("atls: gpu evidence nonce mismatch")
}
verifier, err := v.newGPUVerifier()
if err != nil {
return err
}
return verifier.VerifyWithCoRIM(claims.GPUExtensions.EvidenceJSON, manifest)
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package atls
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
cocosattestation "github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/ultravioletrs/cocos/pkg/attestation/eat"
"github.com/veraison/corim/corim"
)
type stubVerifier struct {
reports [][]byte
err error
}
func (s *stubVerifier) VerifyWithCoRIM(report []byte, _ *corim.UnsignedCorim) error {
s.reports = append(s.reports, append([]byte(nil), report...))
return s.err
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootOnly(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Empty(t, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_RootAndGPU(t *testing.T) {
root := &stubVerifier{}
gpu := &stubVerifier{}
sessionNonce := []byte("session-nonce")
gpuNonce := deriveExpectedGPUNonce(sessionNonce)
gpuNonceHex := hex.EncodeToString(gpuNonce)
evidenceJSON := fmt.Appendf(nil, `[{"nonce":"%s","evidence":"abc","certificate":"def"}]`, gpuNonceHex)
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: gpuNonce,
EvidenceJSON: evidenceJSON,
},
}))
require.NoError(t, err)
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
assert.Equal(t, [][]byte{evidenceJSON}, gpu.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUNonceMismatch(t *testing.T) {
root := &stubVerifier{}
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: []byte("session-nonce"),
GPUExtensions: &eat.GPUExtensions{
Nonce: []byte("wrong"),
EvidenceJSON: []byte(`[{"nonce":"aabbcc"}]`),
},
}))
require.Error(t, err)
assert.ErrorContains(t, err, "gpu nonce binding mismatch")
assert.Equal(t, [][]byte{[]byte("root-report")}, root.reports)
}
func TestPolicyEvidenceVerifierVerifyEvidence_GPUVerifierError(t *testing.T) {
expectedErr := errors.New("gpu verify failed")
root := &stubVerifier{}
gpu := &stubVerifier{err: expectedErr}
sessionNonce := []byte("session-nonce")
derivedNonce := deriveExpectedGPUNonce(sessionNonce)
gpuEvidenceJSON := fmt.Appendf(nil, `[{"nonce":"%s"}]`, hex.EncodeToString(derivedNonce))
v := &policyEvidenceVerifier{
policyPath: "/tmp/policy",
loadManifest: func(string) (*corim.UnsignedCorim, error) {
return &corim.UnsignedCorim{}, nil
},
rootVerifier: func(cocosattestation.PlatformType) (cocosattestation.Verifier, error) {
return root, nil
},
newGPUVerifier: func() (cocosattestation.Verifier, error) {
return gpu, nil
},
}
err := v.VerifyEvidence(encodeClaims(t, &eat.EATClaims{
PlatformType: "TDX",
RawReport: []byte("root-report"),
Nonce: sessionNonce,
GPUExtensions: &eat.GPUExtensions{
Nonce: derivedNonce,
EvidenceJSON: gpuEvidenceJSON,
},
}))
require.Error(t, err)
assert.ErrorIs(t, err, expectedErr)
}
func encodeClaims(t *testing.T, claims *eat.EATClaims) []byte {
t.Helper()
b, err := cbor.Marshal(claims)
require.NoError(t, err)
return b
}
func deriveExpectedGPUNonce(sessionNonce []byte) []byte {
sum := sha256.Sum256(append(append([]byte(nil), sessionNonce...), []byte(":gpu")...))
return sum[:]
}
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"time"
)
func selfSignedCert(t *testing.T) tls.Certificate {
func selfSignedCert(t testing.TB) tls.Certificate {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+25
View File
@@ -4,6 +4,10 @@
package azure
import (
"os"
"strings"
"time"
"github.com/edgelesssys/go-azguestattestation/maa"
)
@@ -28,6 +32,27 @@ func InitializeDefaultMAAVars(config *EnvConfig) {
maa.OSType = config.OSType
maa.OSDistro = config.OSDistro
MaaURL = config.MaaURL
InitializeDefaultAzureTDXVarsFromEnv()
}
func InitializeDefaultAzureTDXVars(imdsURL string, hclRefreshDelay time.Duration) {
if imdsURL = strings.TrimSpace(imdsURL); imdsURL != "" {
azureTDXIMDSQuoteURL = imdsURL
}
if hclRefreshDelay >= 0 {
azureTDXHCLRefreshDelay = hclRefreshDelay
}
}
func InitializeDefaultAzureTDXVarsFromEnv() {
imdsURL := os.Getenv("AZURE_TDX_IMDS_URL")
hclRefreshDelay := azureTDXHCLRefreshDelay
if value := strings.TrimSpace(os.Getenv("AZURE_HCL_REFRESH_WAIT")); value != "" {
if parsed, err := time.ParseDuration(value); err == nil {
hclRefreshDelay = parsed
}
}
InitializeDefaultAzureTDXVars(imdsURL, hclRefreshDelay)
}
func (c *EnvConfig) InitializeOSVars(build, osType, osDistro string) {
+32
View File
@@ -5,6 +5,7 @@ package azure
import (
"testing"
"time"
"github.com/edgelesssys/go-azguestattestation/maa"
"github.com/stretchr/testify/assert"
@@ -56,3 +57,34 @@ func TestInitializeOSVars(t *testing.T) {
assert.Equal(t, "TypeY", cfg.OSType)
assert.Equal(t, "DistroZ", cfg.OSDistro)
}
func TestInitializeDefaultAzureTDXVars(t *testing.T) {
oldURL := azureTDXIMDSQuoteURL
oldDelay := azureTDXHCLRefreshDelay
defer func() {
azureTDXIMDSQuoteURL = oldURL
azureTDXHCLRefreshDelay = oldDelay
}()
InitializeDefaultAzureTDXVars(" https://imds.example/tdquote ", 1500*time.Millisecond)
assert.Equal(t, "https://imds.example/tdquote", azureTDXIMDSQuoteURL)
assert.Equal(t, 1500*time.Millisecond, azureTDXHCLRefreshDelay)
}
func TestInitializeDefaultAzureTDXVarsFromEnv(t *testing.T) {
oldURL := azureTDXIMDSQuoteURL
oldDelay := azureTDXHCLRefreshDelay
defer func() {
azureTDXIMDSQuoteURL = oldURL
azureTDXHCLRefreshDelay = oldDelay
}()
t.Setenv("AZURE_TDX_IMDS_URL", "https://env-imds.example/tdquote")
t.Setenv("AZURE_HCL_REFRESH_WAIT", "2s")
InitializeDefaultAzureTDXVarsFromEnv()
assert.Equal(t, "https://env-imds.example/tdquote", azureTDXIMDSQuoteURL)
assert.Equal(t, 2*time.Second, azureTDXHCLRefreshDelay)
}
+25 -3
View File
@@ -10,7 +10,7 @@ import (
"io"
"net/http"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/magistrala/pkg/errors"
"github.com/edgelesssys/go-azguestattestation/maa"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/tools/lib/report"
@@ -52,6 +52,10 @@ func NewProvider() attestation.Provider {
}
func (a provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error) {
if isAzureTDX() {
return a.TeeAttestation(teeNonce)
}
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
@@ -77,6 +81,10 @@ func (a provider) Attestation(teeNonce []byte, vTpmNonce []byte) ([]byte, error)
}
func (a provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
if isAzureTDX() {
return fetchAzureTDXQuote(teeNonce)
}
var tokenNonce [vtpm.Nonce]byte
copy(tokenNonce[:], teeNonce)
@@ -89,7 +97,6 @@ func (a provider) TeeAttestation(teeNonce []byte) ([]byte, error) {
}
func (a provider) VTpmAttestation(vTpmNonce []byte) ([]byte, error) {
fmt.Printf("DEBUG: VTpmAttestation: vtpm.ExternalTPM is %T at %p\n", vtpm.ExternalTPM, &vtpm.ExternalTPM)
quote, err := vtpm.FetchQuote(vTpmNonce)
if err != nil {
return []byte{}, errors.Wrap(vtpm.ErrFetchQuote, err)
@@ -111,6 +118,14 @@ func (c *defaultMaaClient) Attest(ctx context.Context, nonce []byte, maaURL stri
var DefaultMaaClient MaaClient = &defaultMaaClient{}
func (a provider) AzureAttestationToken(tokenNonce []byte) ([]byte, error) {
if isAzureTDX() {
token, err := FetchAzureTDXAttestationToken(tokenNonce, MaaURL)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
}
return token, nil
}
token, err := DefaultMaaClient.Attest(context.Background(), tokenNonce, MaaURL, http.DefaultClient)
if err != nil {
return nil, errors.Wrap(ErrFetchAzureToken, err)
@@ -152,12 +167,19 @@ func (v verifier) VerifyEAT(eatToken []byte, teeNonce []byte, vTpmNonce []byte)
func (v verifier) VerifyWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
attestation := &attest.Attestation{}
if err := proto.Unmarshal(report, attestation); err != nil {
return fmt.Errorf("failed to unmarshal attestation report: %w", err)
tdxErr := verifyTDXQuoteWithCoRIM(report, manifest)
if tdxErr == nil {
return nil
}
return fmt.Errorf("failed to unmarshal attestation report: %w; Azure TDX verification failed: %v", err, tdxErr)
}
// Extract measurement from SEV-SNP report if present
snpRep := attestation.GetSevSnpAttestation()
if snpRep == nil {
if tdxErr := verifyTDXQuoteWithCoRIM(report, manifest); tdxErr == nil {
return nil
}
return fmt.Errorf("no SEV-SNP attestation found in report")
}
+683
View File
@@ -0,0 +1,683 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
tdxabi "github.com/google/go-tdx-guest/abi"
tdxpb "github.com/google/go-tdx-guest/proto/tdx"
"github.com/google/go-tpm/legacy/tpm2"
"github.com/google/go-tpm/tpmutil"
"github.com/veraison/corim/comid"
"github.com/veraison/corim/corim"
)
const (
tdxAttestEndpoint = "attest/TdxVm"
tdxAPIVersion = "2025-06-01"
tdxRuntimeBinary = "Binary"
tdxRuntimeJSON = "JSON"
azureHCLReportNVIndex = 0x01400001
azureHCLReportDataNVIndex = 0x01400002
azureHCLSignature = "HCLA"
azureHCLVersion = 2
azureHCLRequestType = 2
azureHCLRuntimeDataVersion = 1
azureHCLHashSHA256 = 1
azureHCLReportTypeSNP = 2
azureHCLReportTypeTDX = 4
azureHCLHeaderSize = 0x20
azureHCLMaxHWReportSize = 0x4a0
azureHCLRuntimeDataOffset = azureHCLHeaderSize + azureHCLMaxHWReportSize
azureHCLRuntimeClaimsOffset = 0x14
azureTDReportSize = 0x400
azureTDReportDataOffset = 0x80
)
var (
azureTDXHCLReportReader = readAzureHCLReport
azureTDXReportDataWriter = writeAzureTDXReportData
azureTDXHCLRefreshDelay = 3 * time.Second
azureTDXIMDSQuoteURL = "http://169.254.169.254/acc/tdquote"
)
// TDXQuoteFetcher fetches a raw TDX quote for the provided REPORT_DATA.
type TDXQuoteFetcher interface {
FetchQuote(reportData [tdxabi.ReportDataSize]byte) ([]byte, error)
}
type TDXEvidenceFetcher interface {
FetchEvidence(reportData [tdxabi.ReportDataSize]byte) (*azureTDXEvidence, error)
}
type defaultTDXQuoteFetcher struct{}
func (f defaultTDXQuoteFetcher) FetchQuote(reportData [tdxabi.ReportDataSize]byte) ([]byte, error) {
evidence, err := f.FetchEvidence(reportData)
if err != nil {
return nil, err
}
return evidence.Quote, nil
}
func (f defaultTDXQuoteFetcher) FetchEvidence(reportData [tdxabi.ReportDataSize]byte) (*azureTDXEvidence, error) {
hclReport, err := readFreshAzureTDXHCLReport(reportData)
if err != nil {
return nil, err
}
parsedReport, err := parseAzureHCLReport(hclReport)
if err != nil {
return nil, err
}
if parsedReport.reportType != azureHCLReportTypeTDX {
return nil, fmt.Errorf("Azure HCL report is not TDX")
}
if err := validateAzureTDXRuntimeClaimsHash(parsedReport); err != nil {
return nil, err
}
quote, err := DefaultAzureTDXIMDSClient.GetQuote(context.Background(), parsedReport.hwReport, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("failed to get Azure TDX quote from IMDS: %w", err)
}
return &azureTDXEvidence{
Quote: quote,
RuntimeData: append([]byte(nil), parsedReport.runtimeClaims...),
}, nil
}
// DefaultTDXQuoteFetcher is used by the Azure TDX provider and is replaceable in tests.
var DefaultTDXQuoteFetcher TDXQuoteFetcher = defaultTDXQuoteFetcher{}
// AzureTDXIMDSClient fetches an Azure TDX quote from the Azure Instance Metadata Service.
type AzureTDXIMDSClient interface {
GetQuote(ctx context.Context, tdReport []byte, client *http.Client) ([]byte, error)
}
type defaultAzureTDXIMDSClient struct{}
// DefaultAzureTDXIMDSClient is used by the Azure TDX quote fetcher and is replaceable in tests.
var DefaultAzureTDXIMDSClient AzureTDXIMDSClient = &defaultAzureTDXIMDSClient{}
// AzureTDXClient submits Azure TDX VM attestation evidence to Microsoft Azure Attestation.
type AzureTDXClient interface {
AttestTDXVM(ctx context.Context, quote []byte, runtimeData []byte, nonce []byte, maaURL string, client *http.Client) (string, error)
}
type defaultAzureTDXClient struct{}
// DefaultAzureTDXClient is used by Azure TDX token fetching and is replaceable in tests.
var DefaultAzureTDXClient AzureTDXClient = &defaultAzureTDXClient{}
type tdxAttestRequest struct {
Quote string `json:"quote"`
RuntimeData *tdxDataBlob `json:"runtimeData,omitempty"`
Nonce string `json:"nonce,omitempty"`
}
type tdxDataBlob struct {
Data string `json:"data"`
DataType string `json:"dataType"`
}
type tdxAttestResponse struct {
Token string `json:"token"`
}
type tdxIMDSQuoteRequest struct {
Report string `json:"report"`
}
type tdxIMDSQuoteResponse struct {
Quote string `json:"quote"`
}
type azureTDXEvidence struct {
Quote []byte
RuntimeData []byte
}
type azureHCLReport struct {
reportType uint32
hashType uint32
hwReport []byte
runtimeClaims []byte
}
func isAzureTDX() bool {
if azureTDXHCLReportReader == nil {
return false
}
hclReport, err := azureTDXHCLReportReader()
if err != nil {
return false
}
parsedReport, err := parseAzureHCLReport(hclReport)
return err == nil && parsedReport.reportType == azureHCLReportTypeTDX
}
func fetchAzureTDXQuote(teeNonce []byte) ([]byte, error) {
if teeNonce == nil {
return nil, fmt.Errorf("tee nonce is required for Azure TDX attestation")
}
if len(teeNonce) != tdxabi.ReportDataSize {
return nil, fmt.Errorf("invalid tee nonce length: expected %d bytes, got %d bytes", tdxabi.ReportDataSize, len(teeNonce))
}
var reportData [tdxabi.ReportDataSize]byte
copy(reportData[:], teeNonce)
evidence, err := fetchAzureTDXEvidence(reportData, teeNonce)
if err != nil {
return nil, err
}
return evidence.Quote, nil
}
func (c *defaultAzureTDXClient) AttestTDXVM(ctx context.Context, quote []byte, runtimeData []byte, nonce []byte, maaURL string, client *http.Client) (string, error) {
if maaURL == "" {
return "", fmt.Errorf("maaURL is empty")
}
if client == nil {
client = http.DefaultClient
}
maaURL, err := url.JoinPath(maaURL, tdxAttestEndpoint)
if err != nil {
return "", fmt.Errorf("parsing maaURL: %w", err)
}
maaURL += fmt.Sprintf("?api-version=%s", tdxAPIVersion)
attestRequest := tdxAttestRequest{
Quote: base64.RawURLEncoding.EncodeToString(quote),
}
if len(runtimeData) > 0 {
attestRequest.RuntimeData = &tdxDataBlob{
Data: base64.RawURLEncoding.EncodeToString(runtimeData),
DataType: tdxRuntimeDataType(runtimeData),
}
}
if len(nonce) > 0 {
attestRequest.Nonce = base64.RawURLEncoding.EncodeToString(nonce)
}
reqBytes, err := json.Marshal(attestRequest)
if err != nil {
return "", fmt.Errorf("marshaling TDX attestation request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, maaURL, bytes.NewReader(reqBytes))
if err != nil {
return "", fmt.Errorf("creating TDX attestation request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("doing TDX attestation request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if msg, err := io.ReadAll(resp.Body); err == nil && len(msg) > 0 {
return "", fmt.Errorf("MAA returned %v: %s", resp.Status, msg)
}
return "", fmt.Errorf("MAA returned %v", resp.Status)
}
var attestResponse tdxAttestResponse
if err := json.NewDecoder(resp.Body).Decode(&attestResponse); err != nil {
return "", fmt.Errorf("decoding TDX attestation response: %w", err)
}
if attestResponse.Token == "" {
return "", fmt.Errorf("azure TDX attestation token not found in response")
}
return attestResponse.Token, nil
}
func (c *defaultAzureTDXIMDSClient) GetQuote(ctx context.Context, tdReport []byte, client *http.Client) ([]byte, error) {
if len(tdReport) != azureTDReportSize {
return nil, fmt.Errorf("invalid TD report length: expected %d bytes, got %d bytes", azureTDReportSize, len(tdReport))
}
if client == nil {
client = http.DefaultClient
}
quoteRequest := tdxIMDSQuoteRequest{
Report: base64.RawURLEncoding.EncodeToString(tdReport),
}
reqBytes, err := json.Marshal(quoteRequest)
if err != nil {
return nil, fmt.Errorf("marshaling Azure TDX IMDS quote request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, azureTDXIMDSQuoteURL, bytes.NewReader(reqBytes))
if err != nil {
return nil, fmt.Errorf("creating Azure TDX IMDS quote request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("doing Azure TDX IMDS quote request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if msg, err := io.ReadAll(resp.Body); err == nil && len(msg) > 0 {
return nil, fmt.Errorf("Azure TDX IMDS returned %v: %s", resp.Status, msg)
}
return nil, fmt.Errorf("Azure TDX IMDS returned %v", resp.Status)
}
var quoteResponse tdxIMDSQuoteResponse
if err := json.NewDecoder(resp.Body).Decode(&quoteResponse); err != nil {
return nil, fmt.Errorf("decoding Azure TDX IMDS quote response: %w", err)
}
if quoteResponse.Quote == "" {
return nil, fmt.Errorf("Azure TDX IMDS quote not found in response")
}
quote, err := decodeBase64URL(quoteResponse.Quote)
if err != nil {
return nil, fmt.Errorf("decoding Azure TDX IMDS quote: %w", err)
}
return quote, nil
}
// FetchAzureTDXAttestationToken fetches an Azure Attestation token for an Azure TDX VM.
func FetchAzureTDXAttestationToken(tokenNonce []byte, maaURL string) ([]byte, error) {
reportData := tdxReportDataFromRuntimeData(tokenNonce)
evidence, err := fetchAzureTDXEvidence(reportData, tokenNonce)
if err != nil {
return nil, fmt.Errorf("failed to fetch Azure TDX quote: %w", err)
}
token, err := DefaultAzureTDXClient.AttestTDXVM(context.Background(), evidence.Quote, evidence.RuntimeData, tokenNonce, maaURL, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("error fetching azure TDX token: %w", err)
}
return []byte(token), nil
}
func tdxReportDataFromRuntimeData(runtimeData []byte) [tdxabi.ReportDataSize]byte {
hash := sha256.Sum256(runtimeData)
var reportData [tdxabi.ReportDataSize]byte
copy(reportData[:sha256.Size], hash[:])
return reportData
}
func fetchAzureTDXEvidence(reportData [tdxabi.ReportDataSize]byte, fallbackRuntimeData []byte) (*azureTDXEvidence, error) {
if evidenceFetcher, ok := DefaultTDXQuoteFetcher.(TDXEvidenceFetcher); ok {
return evidenceFetcher.FetchEvidence(reportData)
}
quote, err := DefaultTDXQuoteFetcher.FetchQuote(reportData)
if err != nil {
return nil, err
}
return &azureTDXEvidence{
Quote: quote,
RuntimeData: append([]byte(nil), fallbackRuntimeData...),
}, nil
}
func readFreshAzureTDXHCLReport(reportData [tdxabi.ReportDataSize]byte) ([]byte, error) {
if azureTDXReportDataWriter != nil {
if err := azureTDXReportDataWriter(reportData[:]); err != nil {
return nil, fmt.Errorf("writing Azure TDX report data: %w", err)
}
if azureTDXHCLRefreshDelay > 0 {
time.Sleep(azureTDXHCLRefreshDelay)
}
}
if azureTDXHCLReportReader == nil {
return nil, fmt.Errorf("Azure TDX HCL report reader is not configured")
}
hclReport, err := azureTDXHCLReportReader()
if err != nil {
return nil, fmt.Errorf("reading Azure TDX HCL report: %w", err)
}
return hclReport, nil
}
func readAzureHCLReport() ([]byte, error) {
tpm, err := tpm2.OpenTPM()
if err != nil {
return nil, err
}
defer tpm.Close()
return tpm2.NVReadEx(tpm, azureHCLReportNVIndex, tpm2.HandleOwner, "", 0)
}
func writeAzureTDXReportData(data []byte) error {
if len(data) != tdxabi.ReportDataSize {
return fmt.Errorf("invalid Azure TDX report data length: expected %d bytes, got %d bytes", tdxabi.ReportDataSize, len(data))
}
tpm, err := tpm2.OpenTPM()
if err != nil {
return err
}
defer tpm.Close()
return writeAzureTDXReportDataToTPM(tpm, data)
}
func writeAzureTDXReportDataToTPM(tpm io.ReadWriter, data []byte) error {
if len(data) > int(^uint16(0)) {
return fmt.Errorf("Azure TDX report data is too large")
}
if err := ensureAzureTDXReportDataIndex(tpm, uint16(len(data))); err != nil {
return err
}
if err := tpm2.NVWrite(tpm, tpm2.HandleOwner, azureHCLReportDataNVIndex, "", tpmutil.U16Bytes(data), 0); err != nil {
return fmt.Errorf("writing Azure TDX report-data NV index: %w", err)
}
return nil
}
func ensureAzureTDXReportDataIndex(tpm io.ReadWriter, size uint16) error {
pub, err := tpm2.NVReadPublic(tpm, azureHCLReportDataNVIndex)
if err == nil {
if pub.DataSize == size {
return nil
}
if err := tpm2.NVUndefineSpace(tpm, "", tpm2.HandleOwner, azureHCLReportDataNVIndex); err != nil {
return fmt.Errorf("undefining mismatched Azure TDX report-data NV index: %w", err)
}
}
nvPub := tpm2.NVPublic{
NVIndex: azureHCLReportDataNVIndex,
NameAlg: tpm2.AlgSHA256,
Attributes: tpm2.AttrOwnerWrite | tpm2.AttrOwnerRead,
DataSize: size,
}
authArea := tpm2.AuthCommand{
Session: tpm2.HandlePasswordSession,
Attributes: tpm2.AttrContinueSession,
Auth: []byte(""),
}
if err := tpm2.NVDefineSpaceEx(tpm, tpm2.HandleOwner, "", nvPub, authArea); err != nil {
return fmt.Errorf("defining Azure TDX report-data NV index: %w", err)
}
return nil
}
func parseAzureHCLReport(report []byte) (*azureHCLReport, error) {
minSize := azureHCLRuntimeDataOffset + azureHCLRuntimeClaimsOffset
if len(report) < minSize {
return nil, fmt.Errorf("invalid Azure HCL report size: expected at least %d bytes, got %d bytes", minSize, len(report))
}
if string(report[:len(azureHCLSignature)]) != azureHCLSignature {
return nil, fmt.Errorf("invalid Azure HCL report signature")
}
if version := binary.LittleEndian.Uint32(report[4:8]); version != azureHCLVersion {
return nil, fmt.Errorf("invalid Azure HCL report version: expected %d, got %d", azureHCLVersion, version)
}
reportSize := binary.LittleEndian.Uint32(report[8:12])
if reportSize > uint32(len(report)) {
return nil, fmt.Errorf("invalid Azure HCL report size: header reports %d bytes, got %d bytes", reportSize, len(report))
}
if requestType := binary.LittleEndian.Uint32(report[12:16]); requestType != azureHCLRequestType {
return nil, fmt.Errorf("invalid Azure HCL report request type: expected %d, got %d", azureHCLRequestType, requestType)
}
runtimeData := report[azureHCLRuntimeDataOffset:]
dataSize := binary.LittleEndian.Uint32(runtimeData[0:4])
if dataSize < azureHCLRuntimeClaimsOffset {
return nil, fmt.Errorf("invalid Azure HCL runtime data size: %d", dataSize)
}
if azureHCLRuntimeDataOffset+int(dataSize) > len(report) {
return nil, fmt.Errorf("invalid Azure HCL runtime data size: header reports %d bytes", dataSize)
}
if version := binary.LittleEndian.Uint32(runtimeData[4:8]); version != azureHCLRuntimeDataVersion {
return nil, fmt.Errorf("invalid Azure HCL runtime data version: expected %d, got %d", azureHCLRuntimeDataVersion, version)
}
reportType := binary.LittleEndian.Uint32(runtimeData[8:12])
if reportType != azureHCLReportTypeSNP && reportType != azureHCLReportTypeTDX {
return nil, fmt.Errorf("invalid Azure HCL report type: %d", reportType)
}
hashType := binary.LittleEndian.Uint32(runtimeData[12:16])
claimsSize := binary.LittleEndian.Uint32(runtimeData[16:20])
claimsEnd := azureHCLRuntimeClaimsOffset + int(claimsSize)
if claimsEnd > int(dataSize) {
return nil, fmt.Errorf("invalid Azure HCL runtime claims size: %d", claimsSize)
}
hwReportSize := azureHCLMaxHWReportSize
if reportType == azureHCLReportTypeTDX {
hwReportSize = azureTDReportSize
}
if azureHCLHeaderSize+hwReportSize > len(report) {
return nil, fmt.Errorf("invalid Azure HCL hardware report size: %d", hwReportSize)
}
return &azureHCLReport{
reportType: reportType,
hashType: hashType,
hwReport: append([]byte(nil), report[azureHCLHeaderSize:azureHCLHeaderSize+hwReportSize]...),
runtimeClaims: append([]byte(nil), runtimeData[azureHCLRuntimeClaimsOffset:claimsEnd]...),
}, nil
}
func validateAzureTDXRuntimeClaimsHash(report *azureHCLReport) error {
if report.hashType != azureHCLHashSHA256 {
return fmt.Errorf("unsupported Azure HCL runtime data hash type: %d", report.hashType)
}
if len(report.hwReport) < azureTDReportDataOffset+sha256.Size {
return fmt.Errorf("invalid Azure TDX TD report size: %d", len(report.hwReport))
}
hash := sha256.Sum256(report.runtimeClaims)
if !bytes.Equal(hash[:], report.hwReport[azureTDReportDataOffset:azureTDReportDataOffset+sha256.Size]) {
return fmt.Errorf("Azure TDX runtime claims hash does not match TD report data")
}
return nil
}
func tdxRuntimeDataType(runtimeData []byte) string {
if json.Valid(runtimeData) {
return tdxRuntimeJSON
}
return tdxRuntimeBinary
}
func decodeBase64URL(value string) ([]byte, error) {
decoded, err := base64.RawURLEncoding.DecodeString(value)
if err == nil {
return decoded, nil
}
return base64.URLEncoding.DecodeString(value)
}
func verifyTDXQuoteWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
decodedQuote, err := tdxabi.QuoteToProto(report)
if err != nil {
return fmt.Errorf("failed to parse TDX quote: %w", err)
}
var mrtd []byte
switch q := decodedQuote.(type) {
case *tdxpb.QuoteV4:
tdReport := q.GetTdQuoteBody()
if tdReport == nil {
return fmt.Errorf("missing TDX quote body")
}
mrtd = tdReport.GetMrTd()
case *tdxpb.QuoteV5:
bodyDesc := q.GetTdQuoteBodyDescriptor()
if bodyDesc == nil {
return fmt.Errorf("missing TDX quote body descriptor")
}
tdReport := bodyDesc.GetTdQuoteBodyV5()
if tdReport == nil {
return fmt.Errorf("missing TDX quote body V5")
}
mrtd = tdReport.GetMrTd()
default:
return fmt.Errorf("unsupported TDX quote format")
}
if len(mrtd) == 0 {
return fmt.Errorf("no MRTD in TDX quote")
}
if err := matchMeasurementInCoRIM(manifest, mrtd); err != nil {
return fmt.Errorf("%w for Azure TDX", err)
}
return nil
}
func matchMeasurementInCoRIM(manifest *corim.UnsignedCorim, measurement []byte) error {
if manifest == nil || len(manifest.Tags) == 0 {
return fmt.Errorf("no tags in CoRIM")
}
for _, tag := range manifest.Tags {
if !bytes.HasPrefix(tag, corim.ComidTag) {
continue
}
tagValue := tag[len(corim.ComidTag):]
var c comid.Comid
if err := c.FromCBOR(tagValue); err != nil {
return fmt.Errorf("failed to parse CoMID: %w", err)
}
if c.Triples.ReferenceValues == nil {
continue
}
for _, rv := range *c.Triples.ReferenceValues {
for _, m := range rv.Measurements {
if m.Val.Digests == nil {
continue
}
for _, digest := range *m.Val.Digests {
if bytes.Equal(digest.HashValue, measurement) {
return nil
}
}
}
}
}
return fmt.Errorf("no matching reference value found in CoRIM")
}
// AzureTDXMeasurementData contains the fields extracted from an Azure TDX attestation token
// needed to construct a CoRIM policy for the TDX platform.
type AzureTDXMeasurementData struct {
MRTD string
MRSEAM string
RTMRs []string
SEAMSVN uint64
}
// ExtractAzureTDXMeasurement extracts core TDX measurements from an Azure Attestation token.
func ExtractAzureTDXMeasurement(token string) (*AzureTDXMeasurementData, error) {
claims, err := DefaultValidator.Validate(token)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %w", err)
}
mrtd, ok := azureClaimString(claims, "tdx_mrtd")
if !ok {
return nil, fmt.Errorf("failed to get MRTD from claims")
}
mrSeam, _ := azureClaimString(claims, "tdx_mrseam")
rtmrs := make([]string, 0, 4)
for _, name := range []string{"tdx_rtmr0", "tdx_rtmr1", "tdx_rtmr2", "tdx_rtmr3"} {
if value, ok := azureClaimString(claims, name); ok {
rtmrs = append(rtmrs, value)
}
}
seamSVN, _ := azureClaimUint64(claims, "tdx_seamsvn")
return &AzureTDXMeasurementData{
MRTD: mrtd,
MRSEAM: mrSeam,
RTMRs: rtmrs,
SEAMSVN: seamSVN,
}, nil
}
func azureClaimString(claims map[string]any, name string) (string, bool) {
if value, ok := claims[name].(string); ok {
return value, true
}
tee, ok := claims["x-ms-isolation-tee"].(map[string]any)
if !ok {
return "", false
}
value, ok := tee[name].(string)
return value, ok
}
func azureClaimUint64(claims map[string]any, name string) (uint64, bool) {
value, ok := claims[name]
if !ok {
tee, teeOK := claims["x-ms-isolation-tee"].(map[string]any)
if !teeOK {
return 0, false
}
value, ok = tee[name]
if !ok {
return 0, false
}
}
switch typed := value.(type) {
case float64:
return uint64(typed), true
case int:
return uint64(typed), true
case uint64:
return typed, true
default:
return 0, false
}
}
+455
View File
@@ -0,0 +1,455 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package azure
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
tdxabi "github.com/google/go-tdx-guest/abi"
tdxpb "github.com/google/go-tdx-guest/proto/tdx"
tdxtestdata "github.com/google/go-tdx-guest/testing/testdata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/veraison/corim/comid"
"github.com/veraison/corim/corim"
"github.com/veraison/swid"
)
type mockTDXQuoteFetcher struct {
quote []byte
err error
gotReportData [tdxabi.ReportDataSize]byte
fetchQuoteCall bool
}
func (m *mockTDXQuoteFetcher) FetchQuote(reportData [tdxabi.ReportDataSize]byte) ([]byte, error) {
m.fetchQuoteCall = true
m.gotReportData = reportData
if m.err != nil {
return nil, m.err
}
return m.quote, nil
}
type mockTDXEvidenceFetcher struct {
evidence *azureTDXEvidence
err error
gotReportData [tdxabi.ReportDataSize]byte
fetchEvidenceHit bool
}
func (m *mockTDXEvidenceFetcher) FetchQuote(reportData [tdxabi.ReportDataSize]byte) ([]byte, error) {
evidence, err := m.FetchEvidence(reportData)
if err != nil {
return nil, err
}
return evidence.Quote, nil
}
func (m *mockTDXEvidenceFetcher) FetchEvidence(reportData [tdxabi.ReportDataSize]byte) (*azureTDXEvidence, error) {
m.fetchEvidenceHit = true
m.gotReportData = reportData
if m.err != nil {
return nil, m.err
}
return m.evidence, nil
}
type mockAzureTDXClient struct {
token string
err error
gotQuote []byte
gotRuntime []byte
gotNonce []byte
gotMaaURL string
attestCalls int
}
func (m *mockAzureTDXClient) AttestTDXVM(_ context.Context, quote []byte, runtimeData []byte, nonce []byte, maaURL string, _ *http.Client) (string, error) {
m.attestCalls++
m.gotQuote = append([]byte(nil), quote...)
m.gotRuntime = append([]byte(nil), runtimeData...)
m.gotNonce = append([]byte(nil), nonce...)
m.gotMaaURL = maaURL
if m.err != nil {
return "", m.err
}
return m.token, nil
}
type mockAzureTDXIMDSClient struct {
quote []byte
err error
gotTDReport []byte
getQuoteCall bool
}
func (m *mockAzureTDXIMDSClient) GetQuote(_ context.Context, tdReport []byte, _ *http.Client) ([]byte, error) {
m.getQuoteCall = true
m.gotTDReport = append([]byte(nil), tdReport...)
if m.err != nil {
return nil, m.err
}
return m.quote, nil
}
func testAzureHCLReport(reportType uint32, runtimeClaims []byte) []byte {
reportSize := azureHCLRuntimeDataOffset + azureHCLRuntimeClaimsOffset + len(runtimeClaims)
hclReport := make([]byte, reportSize)
copy(hclReport[:len(azureHCLSignature)], azureHCLSignature)
binary.LittleEndian.PutUint32(hclReport[4:8], azureHCLVersion)
binary.LittleEndian.PutUint32(hclReport[8:12], uint32(reportSize))
binary.LittleEndian.PutUint32(hclReport[12:16], azureHCLRequestType)
runtimeData := hclReport[azureHCLRuntimeDataOffset:]
binary.LittleEndian.PutUint32(runtimeData[0:4], uint32(azureHCLRuntimeClaimsOffset+len(runtimeClaims)))
binary.LittleEndian.PutUint32(runtimeData[4:8], azureHCLRuntimeDataVersion)
binary.LittleEndian.PutUint32(runtimeData[8:12], reportType)
binary.LittleEndian.PutUint32(runtimeData[12:16], azureHCLHashSHA256)
binary.LittleEndian.PutUint32(runtimeData[16:20], uint32(len(runtimeClaims)))
copy(runtimeData[azureHCLRuntimeClaimsOffset:], runtimeClaims)
if reportType == azureHCLReportTypeTDX {
hash := sha256.Sum256(runtimeClaims)
copy(hclReport[azureHCLHeaderSize+azureTDReportDataOffset:], hash[:])
}
return hclReport
}
func TestProvider_TeeAttestation_AzureTDX(t *testing.T) {
oldReader := azureTDXHCLReportReader
oldFetcher := DefaultTDXQuoteFetcher
defer func() {
azureTDXHCLReportReader = oldReader
DefaultTDXQuoteFetcher = oldFetcher
}()
azureTDXHCLReportReader = func() ([]byte, error) {
return testAzureHCLReport(azureHCLReportTypeTDX, []byte(`{"keys":[]}`)), nil
}
fetcher := &mockTDXQuoteFetcher{quote: []byte("tdx-quote")}
DefaultTDXQuoteFetcher = fetcher
reportData := bytes.Repeat([]byte{0xAB}, tdxabi.ReportDataSize)
got, err := NewProvider().TeeAttestation(reportData)
require.NoError(t, err)
assert.Equal(t, []byte("tdx-quote"), got)
assert.True(t, fetcher.fetchQuoteCall)
assert.Equal(t, reportData, fetcher.gotReportData[:])
}
func TestProvider_TeeAttestation_AzureTDX_InvalidNonce(t *testing.T) {
oldReader := azureTDXHCLReportReader
defer func() { azureTDXHCLReportReader = oldReader }()
azureTDXHCLReportReader = func() ([]byte, error) {
return testAzureHCLReport(azureHCLReportTypeTDX, []byte(`{"keys":[]}`)), nil
}
_, err := NewProvider().TeeAttestation([]byte("short"))
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid tee nonce length")
}
func TestProvider_AzureAttestationToken_AzureTDX(t *testing.T) {
oldReader := azureTDXHCLReportReader
oldFetcher := DefaultTDXQuoteFetcher
oldClient := DefaultAzureTDXClient
oldMaaURL := MaaURL
defer func() {
azureTDXHCLReportReader = oldReader
DefaultTDXQuoteFetcher = oldFetcher
DefaultAzureTDXClient = oldClient
MaaURL = oldMaaURL
}()
azureTDXHCLReportReader = func() ([]byte, error) {
return testAzureHCLReport(azureHCLReportTypeTDX, []byte(`{"keys":[]}`)), nil
}
MaaURL = "https://tdx.example.attest.azure.net"
fetcher := &mockTDXQuoteFetcher{quote: []byte("quote")}
client := &mockAzureTDXClient{token: "tdx-token"}
DefaultTDXQuoteFetcher = fetcher
DefaultAzureTDXClient = client
nonce := []byte("token-nonce")
got, err := NewProvider().AzureAttestationToken(nonce)
require.NoError(t, err)
assert.Equal(t, []byte("tdx-token"), got)
expectedReportData := tdxReportDataFromRuntimeData(nonce)
assert.Equal(t, expectedReportData, fetcher.gotReportData)
assert.Equal(t, []byte("quote"), client.gotQuote)
assert.Equal(t, nonce, client.gotRuntime)
assert.Equal(t, nonce, client.gotNonce)
assert.Equal(t, MaaURL, client.gotMaaURL)
}
func TestFetchAzureTDXAttestationToken_UsesHCLRuntimeClaims(t *testing.T) {
oldFetcher := DefaultTDXQuoteFetcher
oldClient := DefaultAzureTDXClient
defer func() {
DefaultTDXQuoteFetcher = oldFetcher
DefaultAzureTDXClient = oldClient
}()
runtimeClaims := []byte(`{"keys":[],"user-data":"nonce"}`)
fetcher := &mockTDXEvidenceFetcher{
evidence: &azureTDXEvidence{
Quote: []byte("quote"),
RuntimeData: runtimeClaims,
},
}
client := &mockAzureTDXClient{token: "tdx-token"}
DefaultTDXQuoteFetcher = fetcher
DefaultAzureTDXClient = client
nonce := []byte("token-nonce")
got, err := FetchAzureTDXAttestationToken(nonce, "https://tdx.example.attest.azure.net")
require.NoError(t, err)
assert.Equal(t, []byte("tdx-token"), got)
assert.True(t, fetcher.fetchEvidenceHit)
assert.Equal(t, tdxReportDataFromRuntimeData(nonce), fetcher.gotReportData)
assert.Equal(t, runtimeClaims, client.gotRuntime)
assert.Equal(t, nonce, client.gotNonce)
}
func TestFetchAzureTDXAttestationToken_FetchQuoteError(t *testing.T) {
oldFetcher := DefaultTDXQuoteFetcher
defer func() { DefaultTDXQuoteFetcher = oldFetcher }()
DefaultTDXQuoteFetcher = &mockTDXQuoteFetcher{err: fmt.Errorf("quote unavailable")}
_, err := FetchAzureTDXAttestationToken([]byte("nonce"), "https://tdx.example.attest.azure.net")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch Azure TDX quote")
}
func TestDefaultTDXQuoteFetcher_FetchEvidence_AzureHCLIMDS(t *testing.T) {
oldReader := azureTDXHCLReportReader
oldWriter := azureTDXReportDataWriter
oldDelay := azureTDXHCLRefreshDelay
oldIMDSClient := DefaultAzureTDXIMDSClient
defer func() {
azureTDXHCLReportReader = oldReader
azureTDXReportDataWriter = oldWriter
azureTDXHCLRefreshDelay = oldDelay
DefaultAzureTDXIMDSClient = oldIMDSClient
}()
runtimeClaims := []byte(`{"keys":[],"user-data":"fresh"}`)
hclReport := testAzureHCLReport(azureHCLReportTypeTDX, runtimeClaims)
var gotReportData []byte
azureTDXReportDataWriter = func(data []byte) error {
gotReportData = append([]byte(nil), data...)
return nil
}
azureTDXHCLReportReader = func() ([]byte, error) {
return hclReport, nil
}
azureTDXHCLRefreshDelay = 0
imdsClient := &mockAzureTDXIMDSClient{quote: []byte("tdx-quote")}
DefaultAzureTDXIMDSClient = imdsClient
reportData := [tdxabi.ReportDataSize]byte{0xAB}
evidence, err := defaultTDXQuoteFetcher{}.FetchEvidence(reportData)
require.NoError(t, err)
assert.Equal(t, reportData[:], gotReportData)
assert.Equal(t, []byte("tdx-quote"), evidence.Quote)
assert.Equal(t, runtimeClaims, evidence.RuntimeData)
assert.True(t, imdsClient.getQuoteCall)
assert.Len(t, imdsClient.gotTDReport, azureTDReportSize)
}
func TestDefaultAzureTDXClient_AttestTDXVM(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/attest/TdxVm", r.URL.Path)
assert.Equal(t, tdxAPIVersion, r.URL.Query().Get("api-version"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
var req tdxAttestRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
assert.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("quote")), req.Quote)
require.NotNil(t, req.RuntimeData)
assert.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("runtime")), req.RuntimeData.Data)
assert.Equal(t, tdxRuntimeBinary, req.RuntimeData.DataType)
assert.Equal(t, base64.RawURLEncoding.EncodeToString([]byte("nonce")), req.Nonce)
_, _ = w.Write([]byte(`{"token":"tdx-token"}`))
}))
defer server.Close()
token, err := (&defaultAzureTDXClient{}).AttestTDXVM(
context.Background(),
[]byte("quote"),
[]byte("runtime"),
[]byte("nonce"),
server.URL,
server.Client(),
)
require.NoError(t, err)
assert.Equal(t, "tdx-token", token)
}
func TestDefaultAzureTDXClient_AttestTDXVM_JSONRuntimeData(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req tdxAttestRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
require.NotNil(t, req.RuntimeData)
assert.Equal(t, tdxRuntimeJSON, req.RuntimeData.DataType)
_, _ = w.Write([]byte(`{"token":"tdx-token"}`))
}))
defer server.Close()
_, err := (&defaultAzureTDXClient{}).AttestTDXVM(
context.Background(),
[]byte("quote"),
[]byte(`{"keys":[]}`),
[]byte("nonce"),
server.URL,
server.Client(),
)
require.NoError(t, err)
}
func TestDefaultAzureTDXClient_AttestTDXVM_ErrorStatus(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad quote", http.StatusBadRequest)
}))
defer server.Close()
_, err := (&defaultAzureTDXClient{}).AttestTDXVM(context.Background(), []byte("quote"), nil, nil, server.URL, server.Client())
require.Error(t, err)
assert.Contains(t, err.Error(), "MAA returned 400 Bad Request")
}
func TestDefaultAzureTDXIMDSClient_GetQuote(t *testing.T) {
oldURL := azureTDXIMDSQuoteURL
defer func() { azureTDXIMDSQuoteURL = oldURL }()
tdReport := bytes.Repeat([]byte{0xA5}, azureTDReportSize)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
var req tdxIMDSQuoteRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
assert.Equal(t, base64.RawURLEncoding.EncodeToString(tdReport), req.Report)
resp := tdxIMDSQuoteResponse{
Quote: base64.RawURLEncoding.EncodeToString([]byte("quote")),
}
require.NoError(t, json.NewEncoder(w).Encode(resp))
}))
defer server.Close()
azureTDXIMDSQuoteURL = server.URL
quote, err := (&defaultAzureTDXIMDSClient{}).GetQuote(context.Background(), tdReport, server.Client())
require.NoError(t, err)
assert.Equal(t, []byte("quote"), quote)
}
func TestIsAzureTDX_UsesHCLReportType(t *testing.T) {
oldReader := azureTDXHCLReportReader
defer func() { azureTDXHCLReportReader = oldReader }()
azureTDXHCLReportReader = func() ([]byte, error) {
return testAzureHCLReport(azureHCLReportTypeSNP, []byte("runtime")), nil
}
assert.False(t, isAzureTDX())
azureTDXHCLReportReader = func() ([]byte, error) {
return testAzureHCLReport(azureHCLReportTypeTDX, []byte(`{"keys":[]}`)), nil
}
assert.True(t, isAzureTDX())
azureTDXHCLReportReader = func() ([]byte, error) {
return nil, fmt.Errorf("no vTPM")
}
assert.False(t, isAzureTDX())
}
func TestExtractAzureTDXMeasurement_Success(t *testing.T) {
oldValidator := DefaultValidator
defer func() { DefaultValidator = oldValidator }()
DefaultValidator = &mockTokenValidator{
validateFunc: func(token string) (map[string]any, error) {
return map[string]any{
"tdx_mrtd": "mrtd",
"tdx_mrseam": "mrseam",
"tdx_rtmr0": "rtmr0",
"tdx_rtmr1": "rtmr1",
"tdx_rtmr2": "rtmr2",
"tdx_rtmr3": "rtmr3",
"tdx_seamsvn": float64(7),
"unrelatedKey": "ignored",
}, nil
},
}
data, err := ExtractAzureTDXMeasurement("valid-token")
require.NoError(t, err)
assert.Equal(t, &AzureTDXMeasurementData{
MRTD: "mrtd",
MRSEAM: "mrseam",
RTMRs: []string{"rtmr0", "rtmr1", "rtmr2", "rtmr3"},
SEAMSVN: 7,
}, data)
}
func TestVerifier_VerifyWithCoRIM_AzureTDX(t *testing.T) {
decodedQuote, err := tdxabi.QuoteToProto(tdxtestdata.RawQuote)
require.NoError(t, err)
quoteV4, ok := decodedQuote.(*tdxpb.QuoteV4)
require.True(t, ok)
mrtd := quoteV4.GetTdQuoteBody().GetMrTd()
c := comid.NewComid()
c.SetTagIdentity("tdx-tag", 0)
m := comid.MustNewUintMeasurement(uint64(1))
m.AddDigest(swid.Sha384, mrtd)
m.SetRawValueBytes([]byte("raw"), nil)
rv := comid.ReferenceValue{
Environment: comid.Environment{
Class: comid.NewClassOID("1.2.3.4"),
},
Measurements: comid.Measurements{*m},
}
c.AddReferenceValue(rv)
manifest := corim.NewUnsignedCorim()
manifest.SetID("test-tdx-corim")
manifest.AddComid(*c)
err = NewVerifier(&bytes.Buffer{}).VerifyWithCoRIM(tdxtestdata.RawQuote, manifest)
assert.NoError(t, err)
}
+41 -1
View File
@@ -39,6 +39,7 @@ type EATClaims struct {
SNPExtensions *SNPExtensions `json:"x-cocos-sevsnp,omitempty"`
TDXExtensions *TDXExtensions `json:"x-cocos-tdx,omitempty"`
VTPMExtensions *VTPMExtensions `json:"x-cocos-vtpm,omitempty"`
GPUExtensions *GPUExtensions `json:"x-cocos-gpu,omitempty"`
// Original binary report (for verification)
RawReport []byte `json:"raw_report,omitempty"`
@@ -94,6 +95,36 @@ type VTPMExtensions struct {
Quote []byte `json:"quote,omitempty"` // TPM quote
}
// GPUExtensions contains optional GPU attestation evidence that is bound to the
// same attestation session as the root TEE evidence.
type GPUExtensions struct {
Vendor string `json:"vendor,omitempty" cbor:"vendor,omitempty"`
EvidenceFormat string `json:"evidence_format,omitempty" cbor:"evidence_format,omitempty"`
Nonce []byte `json:"nonce,omitempty" cbor:"nonce,omitempty"`
EvidenceJSON []byte `json:"evidence_json,omitempty" cbor:"evidence_json,omitempty"`
}
// ClaimsOption customizes EAT claims after the root platform claims are
// extracted.
type ClaimsOption func(*EATClaims) error
// WithGPU attaches GPU evidence both as a typed extension and as an EAT submod.
func WithGPU(gpu *GPUExtensions) ClaimsOption {
return func(claims *EATClaims) error {
if gpu == nil {
return nil
}
claims.GPUExtensions = gpu
if claims.Submods == nil {
claims.Submods = map[string]interface{}{}
}
claims.Submods["gpu"] = gpu
return nil
}
}
// DebugStatus constants (RFC 9711 Section 4.2.6).
const (
DebugEnabled = 0 // Debug is enabled
@@ -112,7 +143,7 @@ const (
const MinNonceLength = 8
// NewEATClaims creates EAT claims from binary attestation report.
func NewEATClaims(report []byte, nonce []byte, platformType attestation.PlatformType) (*EATClaims, error) {
func NewEATClaims(report []byte, nonce []byte, platformType attestation.PlatformType, opts ...ClaimsOption) (*EATClaims, error) {
if len(nonce) < MinNonceLength {
return nil, errors.New("eat_nonce must be at least 8 bytes long")
}
@@ -129,6 +160,15 @@ func NewEATClaims(report []byte, nonce []byte, platformType attestation.Platform
return nil, err
}
for _, opt := range opts {
if opt == nil {
continue
}
if err := opt(claims); err != nil {
return nil, err
}
}
return claims, nil
}
+21
View File
@@ -202,3 +202,24 @@ func TestNewEATClaims_Platforms(t *testing.T) {
})
}
}
func TestNewEATClaims_WithGPU(t *testing.T) {
gpuEvidence := &GPUExtensions{
Vendor: "nvidia",
EvidenceFormat: "nvat-json",
Nonce: []byte("gpu-nonce"),
EvidenceJSON: []byte(`{"evidence":"gpu"}`),
}
claims, err := NewEATClaims(
[]byte("dummy report"),
[]byte("12345678"),
attestation.NoCC,
WithGPU(gpuEvidence),
)
assert.NoError(t, err)
assert.NotNil(t, claims.GPUExtensions)
assert.Equal(t, gpuEvidence, claims.GPUExtensions)
assert.Contains(t, claims.Submods, "gpu")
assert.Equal(t, gpuEvidence, claims.Submods["gpu"])
}
+166 -38
View File
@@ -5,11 +5,16 @@ package eat
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/google/go-sev-guest/abi"
sevsnppb "github.com/google/go-sev-guest/proto/sevsnp"
tdxabi "github.com/google/go-tdx-guest/abi"
tdxpb "github.com/google/go-tdx-guest/proto/tdx"
attestpb "github.com/google/go-tpm-tools/proto/attest"
tpmpb "github.com/google/go-tpm-tools/proto/tpm"
"google.golang.org/protobuf/proto"
)
// OEMID constants (Private Enterprise Numbers).
@@ -20,18 +25,45 @@ const (
)
// extractSNPClaims extracts AMD SEV-SNP specific claims from binary report.
// report may be one of three formats:
// 1. proto-marshaled sevsnp.Attestation (SNP-only platform, from fetchSEVAttestation)
// 2. proto-marshaled attest.Attestation (go-tpm-tools, SNP_VTPM platform, field 7 = SevSnpAttestation)
// 3. raw binary SNP report (0x4A0 bytes)
func extractSNPClaims(claims *EATClaims, report []byte) error {
// Try sevsnp.Attestation (SNP-only proto format).
var sevAttest sevsnppb.Attestation
if err := proto.Unmarshal(report, &sevAttest); err == nil {
if r := sevAttest.GetReport(); r != nil {
return populateSNPClaims(claims, r)
}
}
// Try attest.Attestation (go-tpm-tools SNP_VTPM format).
var tpmAttest attestpb.Attestation
if err := proto.Unmarshal(report, &tpmAttest); err == nil {
if snp := tpmAttest.GetSevSnpAttestation(); snp != nil {
if r := snp.GetReport(); r != nil {
if err := populateSNPClaims(claims, r); err != nil {
return err
}
populateVTPMClaims(claims, tpmAttest.GetQuotes(), tpmAttest.GetEventLog())
return nil
}
}
}
// Fall back to raw binary SNP report.
if len(report) < int(abi.ReportSize) {
return fmt.Errorf("SNP report too small: got %d bytes, want at least %d", len(report), abi.ReportSize)
}
// Parse SNP report structure
snpReport, err := abi.ReportToProto(report[:abi.ReportSize])
if err != nil {
return fmt.Errorf("failed to parse SNP report: %w", err)
}
return populateSNPClaims(claims, snpReport)
}
// Extract SNP-specific fields
func populateSNPClaims(claims *EATClaims, snpReport *sevsnppb.Report) error {
claims.SNPExtensions = &SNPExtensions{
Measurement: snpReport.Measurement,
Policy: snpReport.Policy,
@@ -42,23 +74,16 @@ func extractSNPClaims(claims *EATClaims, report []byte) error {
PlatformInfo: snpReport.PlatformInfo,
ChipID: snpReport.ChipId,
}
// Set TCB version info
claims.SNPExtensions.CurrentTCB = snpReport.CurrentTcb
claims.SNPExtensions.ReportedTCB = snpReport.ReportedTcb
claims.SNPExtensions.CommittedTCB = snpReport.CommittedTcb
claims.SNPExtensions.LaunchTCB = snpReport.LaunchTcb
claims.SNPExtensions.TCB = fmt.Sprintf("current:%d,reported:%d", snpReport.CurrentTcb, snpReport.ReportedTcb)
// Set core EAT claims from SNP report
claims.Measurements = snpReport.Measurement
claims.UEID = snpReport.ChipId // Use ChipID as UEID
claims.OEMID = OEMID_AMD // AMD's PEN (Private Enterprise Number)
claims.SNPExtensions.Signature = snpReport.Signature
// Set hardware model (hash of product name)
claims.Measurements = snpReport.Measurement
claims.UEID = snpReport.ChipId
claims.OEMID = OEMID_AMD
claims.HWModel = []byte(fmt.Sprintf("SEV-SNP-%d", snpReport.Version))
return nil
}
@@ -70,15 +95,59 @@ func extractTDXClaims(claims *EATClaims, report []byte) error {
return fmt.Errorf("failed to parse TDX quote: %w", err)
}
quoteV4, ok := decodedQuote.(*tdxpb.QuoteV4)
if !ok {
var rtmrs [][]byte
var mrTd []byte
var xfam []byte
var tdAttributes []byte
var mrConfigId []byte
var mrOwner []byte
var mrOwnerConfig []byte
var mrSeam []byte
var signature []byte
switch q := decodedQuote.(type) {
case *tdxpb.QuoteV4:
tdReport := q.GetTdQuoteBody()
signedData := q.GetSignedData()
if tdReport == nil {
return fmt.Errorf("missing TDX quote body")
}
rtmrs = tdReport.GetRtmrs()
mrTd = tdReport.GetMrTd()
xfam = tdReport.GetXfam()
tdAttributes = tdReport.GetTdAttributes()
mrConfigId = tdReport.GetMrConfigId()
mrOwner = tdReport.GetMrOwner()
mrOwnerConfig = tdReport.GetMrOwnerConfig()
mrSeam = tdReport.GetMrSeam()
if signedData != nil {
signature = signedData.GetSignature()
}
case *tdxpb.QuoteV5:
bodyDesc := q.GetTdQuoteBodyDescriptor()
if bodyDesc == nil {
return fmt.Errorf("missing TDX quote body descriptor")
}
tdReport := bodyDesc.GetTdQuoteBodyV5()
if tdReport == nil {
return fmt.Errorf("missing TDX quote body V5")
}
rtmrs = tdReport.GetRtmrs()
mrTd = tdReport.GetMrTd()
xfam = tdReport.GetXfam()
tdAttributes = tdReport.GetTdAttributes()
mrConfigId = tdReport.GetMrConfigId()
mrOwner = tdReport.GetMrOwner()
mrOwnerConfig = tdReport.GetMrOwnerConfig()
mrSeam = tdReport.GetMrSeam()
signedData := q.GetSignedData()
if signedData != nil {
signature = signedData.GetSignature()
}
default:
return fmt.Errorf("unsupported TDX quote format")
}
tdReport := quoteV4.GetTdQuoteBody()
signedData := quoteV4.GetSignedData()
rtmrs := tdReport.GetRtmrs()
var rtmr0, rtmr1, rtmr2, rtmr3 []byte
if len(rtmrs) > 0 {
rtmr0 = rtmrs[0]
@@ -94,22 +163,22 @@ func extractTDXClaims(claims *EATClaims, report []byte) error {
}
claims.TDXExtensions = &TDXExtensions{
MRTD: tdReport.GetMrTd(),
MRTD: mrTd,
RTMR0: rtmr0,
RTMR1: rtmr1,
RTMR2: rtmr2,
RTMR3: rtmr3,
XFAM: binary.LittleEndian.Uint64(tdReport.GetXfam()),
TDAttributes: binary.LittleEndian.Uint64(tdReport.GetTdAttributes()),
MRConfigID: tdReport.GetMrConfigId(),
MROwner: tdReport.GetMrOwner(),
MROwnerConfig: tdReport.GetMrOwnerConfig(),
MRSEAM: tdReport.GetMrSeam(),
Signature: signedData.GetSignature(),
XFAM: binary.LittleEndian.Uint64(xfam),
TDAttributes: binary.LittleEndian.Uint64(tdAttributes),
MRConfigID: mrConfigId,
MROwner: mrOwner,
MROwnerConfig: mrOwnerConfig,
MRSEAM: mrSeam,
Signature: signature,
}
// Set core EAT claims
claims.Measurements = tdReport.GetMrTd()
claims.Measurements = mrTd
// Use first 32 bytes of MRTD as UEID, similar to other extractors
if len(claims.Measurements) >= 32 {
claims.UEID = claims.Measurements[:32]
@@ -122,18 +191,77 @@ func extractTDXClaims(claims *EATClaims, report []byte) error {
return nil
}
// extractVTPMClaims extracts vTPM specific claims from binary report.
func extractVTPMClaims(claims *EATClaims, report []byte) error {
// vTPM report is typically a marshaled structure containing PCRs and quote
// For now, store the entire report as the quote
claims.VTPMExtensions = &VTPMExtensions{
Quote: report,
PCRs: make(map[string]string),
// populateVTPMClaims fills VTPMExtensions from go-tpm-tools quote banks and event log.
// For SNP_VTPM the SNP measurement is already set; this adds PCR values alongside it.
// PCR keys are formatted as "<hash>:<index>" (e.g. "sha256:0"), values are hex-encoded.
// The raw TPMS_ATTEST bytes from the SHA-256 bank are stored as the canonical Quote.
func populateVTPMClaims(claims *EATClaims, quotes []*tpmpb.Quote, eventLog []byte) {
vtpm := &VTPMExtensions{
PCRs: make(map[string]string),
EventLog: eventLog,
}
// Set core EAT claims
claims.Measurements = report[:32] // Use first 32 bytes as measurement
claims.UEID = report[:16] // Use first 16 bytes as UEID
for _, q := range quotes {
if q == nil {
continue
}
pcrs := q.GetPcrs()
if pcrs == nil {
continue
}
// Prefer the SHA-256 bank as the canonical raw quote; fall back to the first available.
if pcrs.GetHash() == tpmpb.HashAlgo_SHA256 || vtpm.Quote == nil {
if raw := q.GetQuote(); len(raw) > 0 {
vtpm.Quote = raw
}
}
hashName := tpmHashName(pcrs.GetHash())
for idx, val := range pcrs.GetPcrs() {
vtpm.PCRs[fmt.Sprintf("%s:%d", hashName, idx)] = hex.EncodeToString(val)
}
}
claims.VTPMExtensions = vtpm
}
func tpmHashName(h tpmpb.HashAlgo) string {
switch h {
case tpmpb.HashAlgo_SHA1:
return "sha1"
case tpmpb.HashAlgo_SHA256:
return "sha256"
case tpmpb.HashAlgo_SHA384:
return "sha384"
case tpmpb.HashAlgo_SHA512:
return "sha512"
default:
return fmt.Sprintf("hash%d", int(h))
}
}
// extractVTPMClaims extracts vTPM specific claims from a proto-marshaled attest.Attestation.
func extractVTPMClaims(claims *EATClaims, report []byte) error {
var tpmAttest attestpb.Attestation
if err := proto.Unmarshal(report, &tpmAttest); err != nil {
return fmt.Errorf("failed to parse vTPM attestation: %w", err)
}
populateVTPMClaims(claims, tpmAttest.GetQuotes(), tpmAttest.GetEventLog())
// Use PCR0 (SHA-256) as the canonical measurement if present.
if ext := claims.VTPMExtensions; ext != nil {
if v, ok := ext.PCRs["sha256:0"]; ok {
b, err := hex.DecodeString(v)
if err == nil {
claims.Measurements = b
if len(b) >= 16 {
claims.UEID = b[:16]
}
}
}
}
return nil
}
+22 -6
View File
@@ -9,8 +9,11 @@ import (
"testing"
"github.com/google/go-sev-guest/abi"
attestpb "github.com/google/go-tpm-tools/proto/attest"
tpmpb "github.com/google/go-tpm-tools/proto/tpm"
"github.com/stretchr/testify/assert"
"github.com/ultravioletrs/cocos/pkg/attestation"
"google.golang.org/protobuf/proto"
)
func TestExtractSNPClaims(t *testing.T) {
@@ -99,16 +102,29 @@ func TestTDXExtensionsJSON(t *testing.T) {
}
func TestExtractVTPMClaims(t *testing.T) {
report := make([]byte, 32)
copy(report, []byte("vtpm-report-with-enough-length-123"))
pcr0 := []byte("0123456789abcdef0123456789abcdef")
rawQuote := []byte("raw-vtpm-quote")
report, err := proto.Marshal(&attestpb.Attestation{
Quotes: []*tpmpb.Quote{{
Quote: rawQuote,
Pcrs: &tpmpb.PCRs{
Hash: tpmpb.HashAlgo_SHA256,
Pcrs: map[uint32][]byte{
0: pcr0,
},
},
}},
EventLog: []byte("event-log"),
})
assert.NoError(t, err)
claims := &EATClaims{}
err := extractVTPMClaims(claims, report)
err = extractVTPMClaims(claims, report)
assert.NoError(t, err)
assert.NotNil(t, claims.VTPMExtensions)
assert.Equal(t, report, claims.VTPMExtensions.Quote)
assert.Equal(t, report[:32], claims.Measurements)
assert.Equal(t, report[:16], claims.UEID)
assert.Equal(t, rawQuote, claims.VTPMExtensions.Quote)
assert.Equal(t, pcr0, claims.Measurements)
assert.Equal(t, pcr0[:16], claims.UEID)
}
func TestExtractAzureClaims(t *testing.T) {
+138
View File
@@ -0,0 +1,138 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
)
const (
DefaultVendor = "nvidia"
DefaultEvidenceFormat = "nvat-json"
)
// Collector retrieves GPU evidence for the current attestation session.
type Collector interface {
Collect(ctx context.Context, nonce []byte) (*Evidence, error)
}
// Evidence contains low-level GPU evidence collected out-of-process.
type Evidence struct {
Vendor string
EvidenceFormat string
Nonce []byte
RawEvidence []byte
}
type commandCollector struct {
binaryPath string
timeout time.Duration
execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}
type helperRequest struct {
Mode string `json:"mode,omitempty"`
NonceHex string `json:"nonce_hex"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
}
type helperResponse struct {
Vendor string `json:"vendor,omitempty"`
EvidenceFormat string `json:"evidence_format,omitempty"`
EvidenceJSON json.RawMessage `json:"evidence_json,omitempty"`
ClaimsJSON json.RawMessage `json:"claims_json,omitempty"`
DetachedEATJSON json.RawMessage `json:"detached_eat_json,omitempty"`
}
// NewCommandCollector creates a collector that shells out to a helper binary.
// The helper is expected to read a JSON request on stdin and emit a JSON
// response on stdout. See tools/nvidia-attestation-helper for the contract.
func NewCommandCollector(binaryPath string, timeout time.Duration) (Collector, error) {
if strings.TrimSpace(binaryPath) == "" {
return nil, fmt.Errorf("gpu helper path cannot be empty")
}
if timeout <= 0 {
timeout = 30 * time.Second
}
return &commandCollector{
binaryPath: binaryPath,
timeout: timeout,
execCommandContext: exec.CommandContext,
}, nil
}
func (c *commandCollector) Collect(ctx context.Context, nonce []byte) (*Evidence, error) {
if len(nonce) == 0 {
return nil, fmt.Errorf("gpu nonce cannot be empty")
}
reqBody, err := json.Marshal(helperRequest{
Mode: "collect",
NonceHex: hex.EncodeToString(nonce),
})
if err != nil {
return nil, fmt.Errorf("failed to marshal GPU helper request: %w", err)
}
runCtx := ctx
cancel := func() {}
if c.timeout > 0 {
runCtx, cancel = context.WithTimeout(ctx, c.timeout)
}
defer cancel()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := c.execCommandContext(runCtx, c.binaryPath)
cmd.Stdin = bytes.NewReader(reqBody)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
errMsg = err.Error()
}
return nil, fmt.Errorf("gpu helper failed: %s", errMsg)
}
var resp helperResponse
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
return nil, fmt.Errorf("failed to decode GPU helper response: %w", err)
}
if len(resp.EvidenceJSON) == 0 {
return nil, fmt.Errorf("gpu helper response did not contain evidence_json")
}
vendor := resp.Vendor
if vendor == "" {
vendor = DefaultVendor
}
evidenceFormat := resp.EvidenceFormat
if evidenceFormat == "" {
evidenceFormat = DefaultEvidenceFormat
}
return &Evidence{
Vendor: vendor,
EvidenceFormat: evidenceFormat,
Nonce: append([]byte(nil), nonce...),
RawEvidence: append([]byte(nil), resp.EvidenceJSON...),
}, nil
}
// SetExecCommandContext allows tests to inject a mock exec.CommandContext.
func (c *commandCollector) SetExecCommandContext(cmdFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd) {
c.execCommandContext = cmdFunc
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func fakeExecCommandContext(_ context.Context, name string, arg ...string) *exec.Cmd {
args := append([]string{"-test.run=TestGPUHelperProcess", "--", name}, arg...)
cmd := exec.Command(os.Args[0], args...)
cmd.Env = append(os.Environ(), "GO_WANT_GPU_HELPER_PROCESS=1")
return cmd
}
func TestGPUHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_GPU_HELPER_PROCESS") != "1" {
return
}
args := os.Args
for i := range args {
if args[i] == "--" {
args = args[i+1:]
break
}
}
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "missing helper name")
os.Exit(2)
}
switch args[0] {
case "helper-error":
fmt.Fprintln(os.Stderr, "simulated helper failure")
os.Exit(1)
case "helper-invalid-json":
fmt.Fprintln(os.Stdout, "{not-json")
os.Exit(0)
case "helper-empty-evidence":
fmt.Fprintln(os.Stdout, `{"vendor":"nvidia","evidence_format":"nvat-json"}`)
os.Exit(0)
default:
var req helperRequest
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
if req.Mode != "collect" {
fmt.Fprintln(os.Stderr, "unexpected helper mode")
os.Exit(1)
}
resp := helperResponse{
Vendor: "nvidia",
EvidenceFormat: "nvat-json",
EvidenceJSON: json.RawMessage(fmt.Sprintf(`{"nonce_hex":"%s","evidence":"ok"}`, req.NonceHex)),
}
if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}
}
func TestNewCommandCollector(t *testing.T) {
collector, err := NewCommandCollector("helper", time.Second)
assert.NoError(t, err)
assert.NotNil(t, collector)
collector, err = NewCommandCollector("", time.Second)
assert.Error(t, err)
assert.Nil(t, collector)
}
func TestCommandCollectorCollect(t *testing.T) {
collector, err := NewCommandCollector("helper-success", time.Second)
require.NoError(t, err)
cmdCollector, ok := collector.(*commandCollector)
require.True(t, ok)
cmdCollector.SetExecCommandContext(fakeExecCommandContext)
evidence, err := collector.Collect(context.Background(), []byte{0xaa, 0xbb, 0xcc})
require.NoError(t, err)
assert.Equal(t, DefaultVendor, evidence.Vendor)
assert.Equal(t, DefaultEvidenceFormat, evidence.EvidenceFormat)
assert.Equal(t, []byte{0xaa, 0xbb, 0xcc}, evidence.Nonce)
assert.JSONEq(t, `{"nonce_hex":"aabbcc","evidence":"ok"}`, string(evidence.RawEvidence))
}
func TestCommandCollectorCollectErrors(t *testing.T) {
tests := []struct {
name string
helperName string
wantErr string
}{
{
name: "helper process failure",
helperName: "helper-error",
wantErr: "gpu helper failed: simulated helper failure",
},
{
name: "invalid json response",
helperName: "helper-invalid-json",
wantErr: "failed to decode GPU helper response",
},
{
name: "missing evidence payload",
helperName: "helper-empty-evidence",
wantErr: "gpu helper response did not contain evidence_json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
collector, err := NewCommandCollector(tt.helperName, time.Second)
require.NoError(t, err)
cmdCollector, ok := collector.(*commandCollector)
require.True(t, ok)
cmdCollector.SetExecCommandContext(fakeExecCommandContext)
_, err = collector.Collect(context.Background(), []byte{0xaa})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package gpu
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
"github.com/ultravioletrs/cocos/pkg/attestation"
"github.com/veraison/corim/comid"
"github.com/veraison/corim/corim"
)
const (
DefaultVerifierBinary = "nvidia-attestation-helper"
defaultVerifierTimeout = 30 * time.Second
)
var _ attestation.Verifier = (*verifier)(nil)
type verifier struct {
binaryPath string
timeout time.Duration
execCommandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
}
type evidenceEnvelope struct {
Nonce string `json:"nonce"`
}
// gpuDeviceClaims mirrors the per-device object produced by the NVIDIA
// attestation helper's verify mode (e.g. "GPU-0": { ... }).
type gpuDeviceClaims struct {
HWModel string `json:"hwmodel"`
OEMID string `json:"oemid"`
DriverVersion string `json:"x-nvidia-gpu-driver-version"`
VBIOSVersion string `json:"x-nvidia-gpu-vbios-version"`
SecBoot bool `json:"secboot"`
DebugStatus string `json:"dbgstat"`
MeasurementResult string `json:"measres"`
NonceMatch bool `json:"x-nvidia-gpu-attestation-report-nonce-match"`
SigVerified bool `json:"x-nvidia-gpu-attestation-report-signature-verified"`
FWIDMatch bool `json:"x-nvidia-gpu-attestation-report-cert-chain-fwid-match"`
ArchCheck bool `json:"x-nvidia-gpu-arch-check"`
DriverRIMSigVerified bool `json:"x-nvidia-gpu-driver-rim-signature-verified"`
VBIOSRIMSigVerified bool `json:"x-nvidia-gpu-vbios-rim-signature-verified"`
DriverRIMVersionMatch bool `json:"x-nvidia-gpu-driver-rim-version-match"`
VBIOSRIMVersionMatch bool `json:"x-nvidia-gpu-vbios-rim-version-match"`
AttestationWarning *string `json:"x-nvidia-attestation-warning"`
}
func NewVerifier(binaryPath string, timeout time.Duration) (attestation.Verifier, error) {
if strings.TrimSpace(binaryPath) == "" {
binaryPath = DefaultVerifierBinary
}
if timeout <= 0 {
timeout = defaultVerifierTimeout
}
return &verifier{
binaryPath: binaryPath,
timeout: timeout,
execCommandContext: exec.CommandContext,
}, nil
}
func (v *verifier) VerifyWithCoRIM(report []byte, manifest *corim.UnsignedCorim) error {
if len(report) == 0 {
return fmt.Errorf("gpu evidence is empty")
}
nonceHex, err := evidenceNonce(report)
if err != nil {
return err
}
reqBody, err := json.Marshal(helperRequest{
Mode: "verify",
NonceHex: nonceHex,
EvidenceJSON: append(json.RawMessage(nil), report...),
})
if err != nil {
return fmt.Errorf("failed to marshal GPU verifier request: %w", err)
}
runCtx := context.Background()
cancel := func() {}
if v.timeout > 0 {
runCtx, cancel = context.WithTimeout(runCtx, v.timeout)
}
defer cancel()
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
cmd := v.execCommandContext(runCtx, v.binaryPath)
cmd.Stdin = bytes.NewReader(reqBody)
cmd.Stdout = stdout
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
errMsg := strings.TrimSpace(stderr.String())
if errMsg == "" {
errMsg = err.Error()
}
return fmt.Errorf("gpu verifier helper failed: %s", errMsg)
}
var resp helperResponse
if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
return fmt.Errorf("failed to decode GPU verifier response: %w", err)
}
if len(resp.ClaimsJSON) == 0 {
return fmt.Errorf("gpu verifier response did not contain claims_json")
}
var deviceClaims map[string]gpuDeviceClaims
if err := json.Unmarshal(resp.ClaimsJSON, &deviceClaims); err != nil {
return fmt.Errorf("gpu: failed to parse claims JSON: %w", err)
}
if len(deviceClaims) == 0 {
return fmt.Errorf("gpu: verifier response contained no device claims")
}
return appraiseGPUClaims(deviceClaims, manifest)
}
// appraiseGPUClaims checks mandatory security flags on every device, then
// matches each device's identity against CoRIM reference values when a
// manifest is provided.
func appraiseGPUClaims(devices map[string]gpuDeviceClaims, manifest *corim.UnsignedCorim) error {
for id, c := range devices {
if !c.SecBoot {
return fmt.Errorf("gpu: %s: secure boot not enabled", id)
}
if c.DebugStatus != "disabled" {
return fmt.Errorf("gpu: %s: debug not disabled (got %q)", id, c.DebugStatus)
}
if c.MeasurementResult != "success" {
return fmt.Errorf("gpu: %s: measurement result not success (got %q)", id, c.MeasurementResult)
}
if !c.NonceMatch || !c.SigVerified || !c.FWIDMatch || !c.ArchCheck ||
!c.DriverRIMSigVerified || !c.VBIOSRIMSigVerified ||
!c.DriverRIMVersionMatch || !c.VBIOSRIMVersionMatch {
return fmt.Errorf("gpu: %s: one or more attestation verification flags are false", id)
}
if c.AttestationWarning != nil {
return fmt.Errorf("gpu: %s: attestation warning: %s", id, *c.AttestationWarning)
}
}
if manifest == nil {
return nil
}
// Match each device's identity (model|driver|vbios) against CoRIM digests.
// matchesCoRIM returns true when a digest matches OR when the manifest
// contains no digest entries at all (no GPU policy configured).
for id, c := range devices {
identity := c.HWModel + "|" + c.DriverVersion + "|" + c.VBIOSVersion
digest := sha256.Sum256([]byte(identity))
if !matchesCoRIM(digest[:], manifest) {
return fmt.Errorf("gpu: %s: no CoRIM reference value matched (model=%q driver=%q vbios=%q)",
id, c.HWModel, c.DriverVersion, c.VBIOSVersion)
}
}
return nil
}
// matchesCoRIM returns true when digest matches any reference value digest in
// the manifest, or when the manifest contains no digest entries (treating an
// empty manifest as "no GPU policy configured").
func matchesCoRIM(digest []byte, manifest *corim.UnsignedCorim) bool {
hasAnyDigests := false
for _, tag := range manifest.Tags {
if !bytes.HasPrefix(tag, corim.ComidTag) {
continue
}
var c comid.Comid
if err := c.FromCBOR(tag[len(corim.ComidTag):]); err != nil {
continue
}
if c.Triples.ReferenceValues == nil {
continue
}
for _, rv := range *c.Triples.ReferenceValues {
if rv.Measurements.Valid() != nil {
continue
}
for _, m := range rv.Measurements {
if m.Val.Digests == nil {
continue
}
for _, d := range *m.Val.Digests {
hasAnyDigests = true
if bytes.Equal(d.HashValue, digest) {
return true
}
}
}
}
}
return !hasAnyDigests
}
func evidenceNonce(report []byte) (string, error) {
var envelopes []evidenceEnvelope
if err := json.Unmarshal(report, &envelopes); err != nil {
return "", fmt.Errorf("failed to parse GPU evidence JSON: %w", err)
}
if len(envelopes) == 0 {
return "", fmt.Errorf("gpu evidence did not contain any devices")
}
if strings.TrimSpace(envelopes[0].Nonce) == "" {
return "", fmt.Errorf("gpu evidence nonce is missing")
}
return envelopes[0].Nonce, nil
}
// SetExecCommandContext allows tests to inject a mock exec.CommandContext.
func (v *verifier) SetExecCommandContext(cmdFunc func(ctx context.Context, name string, arg ...string) *exec.Cmd) {
v.execCommandContext = cmdFunc
}

Some files were not shown because too many files have changed in this diff Show More