Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .github/workflows/build-deploy-nitro-enclave.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,29 @@ jobs:
id: create-insecure-eif
run: |
docker load --input /tmp/${{ matrix.app }}-nitro-insecure-image.tar
sudo nitro-cli build-enclave --docker-uri "${{ matrix.nitro_prefix }}-insecure:latest" --output-file go-enclave-outbound-insecure.eif
BUILD_OUTPUT=$(sudo nitro-cli build-enclave --docker-uri "${{ matrix.nitro_prefix }}-insecure:latest" --output-file go-enclave-outbound-insecure.eif)
echo "$BUILD_OUTPUT"
ls -la go-enclave-outbound-insecure.eif

# Extract PCR measurements for the insecure image
MEASUREMENTS_SECTION=$(echo "$BUILD_OUTPUT" | awk '/^{/,/^}$/' | sed 's/^}/}/')
echo "$MEASUREMENTS_SECTION" > pcr_measurements_insecure.json

if [ -f pcr_measurements_insecure.json ]; then
MEASUREMENTS=$(cat pcr_measurements_insecure.json)
PCR0=$(echo "$MEASUREMENTS" | grep -oP '"PCR0": "\K[^"]+' || echo "N/A")
PCR1=$(echo "$MEASUREMENTS" | grep -oP '"PCR1": "\K[^"]+' || echo "N/A")
PCR2=$(echo "$MEASUREMENTS" | grep -oP '"PCR2": "\K[^"]+' || echo "N/A")

echo "**Insecure PCR Measurements [FOR TESTING ONLY] (${{ matrix.app }}):**" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "PCR0: $PCR0" >> $GITHUB_STEP_SUMMARY
echo "PCR1: $PCR1" >> $GITHUB_STEP_SUMMARY
echo "PCR2: $PCR2" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
fi

# Overwrite the EIF so the plugin Dockerfile picks it up
sudo cp go-enclave-outbound-insecure.eif go-enclave-outbound.eif

Expand Down Expand Up @@ -278,7 +299,7 @@ jobs:
- name: confidential-workflows
nitro_prefix: nitro-workflows
plugin_prefix: enclave-workflows
runs-on: [self-hosted, Linux, X64]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
Expand Down
122 changes: 69 additions & 53 deletions .github/workflows/go-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ on:
- cron: '0 7 * * *'
env:
CHAINLINK_COMMIT_SHA: a1ea8cd880345a3a6c80bf70f079c7dc783d5493
LEGACY_COMMIT_SHA: 24ffd2435ed7d6bca45525c2189d720b7a4ef6bc # v0.0.14, the commit pointing to whatever is capability gitref is deployed in Chainlink's plugins.private.yaml: https://github.com/smartcontractkit/chainlink/blob/develop/plugins/plugins.private.yaml#L50.
LEGACY_COMMIT_SHA: 7d70fcf65d8b5af102d8267b54d31ea163dea203 # initial commit, the commit pointing to whatever is capability gitref is deployed in Chainlink's plugins.private.yaml: https://github.com/smartcontractkit/chainlink/blob/develop/plugins/plugins.private.yaml#L50.
LEGACY_ENCLAVE_RELEASE: v1.3.0 # The release version whose enclaves are currently deployed in production. Update this when deploying new enclaves.
# Optional PCR override: when all three are set, the legacy-enclaves leg uses
# these measurements directly instead of fetching them from the release build
# logs. Leave blank to fetch from LEGACY_ENCLAVE_RELEASE.
LEGACY_ENCLAVE_PCR0: 7cf9af0301ac6aff79c3f0c3d069c72ceb75cd6aebf118754de8368023bed3ad51d97c7e370fee0bf83719772be709f8
LEGACY_ENCLAVE_PCR1: 4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493
LEGACY_ENCLAVE_PCR2: e651899c697e2aced754cba2b0cd49c3ae73b0d0fbac104adf93a6e0869bc5237452fcfac5aacbd84dea199121812416
# Pinned refs for the supporting CRE Docker images, used as GHCR cache keys.
JD_VERSION: 0.22.1 # job-distributor release (checked out as v${JD_VERSION})
ATLAS_SHA: da84cb72d3a160e02896247d46ab4b9806ebee2f # chip-ingress + chip-config
Expand All @@ -33,11 +39,11 @@ jobs:
# run the real-enclave e2e + integration suite plus the backwards-compat and
# legacy-enclaves variants; the nightly schedule runs only the real-enclave
# e2e + integration suite. Pull requests exercise fake enclaves for the
# standard and remote-legacy variants. Backwards-compatibility runs a prior-release
# capability plugin, which predates fake enclaves and cannot validate their
# attestation, so for now it runs against real enclaves on the self-hosted
# Nitro runner. TODO(PRIV-506): once a prior release with fake-enclave support
# exists, switch this variant back to fake enclaves on the hosted runners.
# standard and backwards-compat variants, plus the remote legacy-enclaves
# variant. Backwards-compatibility runs a prior-release capability plugin
# against fake enclaves via the confidential-HTTP e2e portion (the prior
# release now supports fake enclaves, so it no longer needs the self-hosted
# Nitro runner).
#
# Applying the "e2e-real-enclaves" label to a PR additionally runs the e2e +
# integration suites against real Nitro enclaves on the self-hosted runner.
Expand Down Expand Up @@ -67,14 +73,14 @@ jobs:
E2E_HTTP='{"name":"E2E confidential HTTP (fake enclaves)","real":false,"legacy":false,"legacy-enclaves":false,"portion":"e2e-http"}'
E2E_WORKFLOWS='{"name":"E2E workflow engine (fake enclaves)","real":false,"legacy":false,"legacy-enclaves":false,"portion":"e2e-workflows"}'
REAL_E2E='{"name":"e2e + integration (real enclaves)","real":true,"legacy":false,"legacy-enclaves":false}'
BACKWARDS_COMPAT='{"name":"Test backwards compatibility (real enclaves)","real":true,"legacy":true,"legacy-enclaves":false}'
BACKWARDS_COMPAT='{"name":"Test backwards compatibility (fake enclaves)","real":false,"legacy":true,"legacy-enclaves":false,"portion":"e2e-http"}'
LEGACY_ENCLAVES='{"name":"Test against legacy enclaves","real":false,"legacy":false,"legacy-enclaves":true}'

# ---- Select variants for the triggering event ----
case "${{ github.event_name }}" in
pull_request)
# Fake-enclave suites for fast feedback, plus the real-enclave
# backwards-compat and remote-legacy variants.
# Fake-enclave suites for fast feedback (including backwards-compat),
# plus the remote legacy-enclaves variant.
VARIANTS=("$UNIT" "$E2E_HTTP" "$E2E_WORKFLOWS" "$BACKWARDS_COMPAT" "$LEGACY_ENCLAVES")
# Opt-in: the "e2e-real-enclaves" label adds a real Nitro e2e +
# integration run on the self-hosted runner.
Expand All @@ -83,9 +89,11 @@ jobs:
fi
;;
push)
# Release-branch pushes: the same real/legacy set a PR runs, minus
# the fake-enclave suites. Pushes to main are gated out by
# run-suite, so this only runs on release/** pushes.
# Release-branch pushes: the real e2e + integration suite plus the
# backwards-compat and remote legacy-enclaves variants (dropping the
# plain fake-enclave unit/e2e suites a PR runs for fast feedback).
# Pushes to main are gated out by run-suite, so this only runs on
# release/** pushes.
VARIANTS=("$REAL_E2E" "$BACKWARDS_COMPAT" "$LEGACY_ENCLAVES")
;;
*)
Expand Down Expand Up @@ -480,9 +488,9 @@ jobs:

test:
name: ${{ matrix.name }}
# Real-enclave variants (the nightly run and the backwards-compatibility
# variant) require the self-hosted Nitro instance; fake-enclave variants run
# on regular GitHub-hosted runners.
# Real-enclave variants (the nightly run and the label-triggered e2e +
# integration suite) require the self-hosted Nitro instance; fake-enclave
# variants run on regular GitHub-hosted runners.
runs-on: ${{ matrix.real && fromJSON('["self-hosted", "Linux", "X64"]') || fromJSON(format('["runs-on={0}", "cpu=16+32", "ram=8+64", "family=c7i+c7a+c6i", "volume=200gb"]', github.run_id)) }}
if: ${{ needs.setup.outputs.run-suite == 'true' }}
needs: [setup, build, prebuild-images]
Expand Down Expand Up @@ -714,44 +722,51 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "Fetching PCR measurements for legacy enclave release ${{ env.LEGACY_ENCLAVE_RELEASE }}..."
if [ -n "${{ env.LEGACY_ENCLAVE_PCR0 }}" ] && [ -n "${{ env.LEGACY_ENCLAVE_PCR1 }}" ] && [ -n "${{ env.LEGACY_ENCLAVE_PCR2 }}" ]; then
echo "Using PCR measurement override (skipping release fetch)..."
PCR0="${{ env.LEGACY_ENCLAVE_PCR0 }}"
PCR1="${{ env.LEGACY_ENCLAVE_PCR1 }}"
PCR2="${{ env.LEGACY_ENCLAVE_PCR2 }}"
else
echo "Fetching PCR measurements for legacy enclave release ${{ env.LEGACY_ENCLAVE_RELEASE }}..."

# Get the release notes to find the workflow run URL
RELEASE_BODY=$(gh release view "${{ env.LEGACY_ENCLAVE_RELEASE }}" --repo "${{ github.repository }}" --json body --jq '.body')
echo "Release body: $RELEASE_BODY"
# Get the release notes to find the workflow run URL
RELEASE_BODY=$(gh release view "${{ env.LEGACY_ENCLAVE_RELEASE }}" --repo "${{ github.repository }}" --json body --jq '.body')
echo "Release body: $RELEASE_BODY"

# Extract the workflow run ID from the release notes URL
RUN_ID=$(echo "$RELEASE_BODY" | grep -oP 'actions/runs/\K[0-9]+' || true)
if [ -z "$RUN_ID" ]; then
echo "::error::Could not find workflow run ID in release notes for ${{ env.LEGACY_ENCLAVE_RELEASE }}"
exit 1
fi
echo "Found workflow run ID: $RUN_ID"

# Get the build-enclave-and-plugin job ID. The build is now a matrix
# across enclave apps, so match the confidential-http job (which builds
# the enclave the CI test connects to). Fall back to the pre-matrix
# single-job name for older releases.
JOB_ID=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs" \
--jq '.jobs[] | select(.name == "build-enclave-and-plugin" or (.name | startswith("build-enclave-and-plugin (confidential-http"))) | .id' | head -1)
echo "Found job ID: $JOB_ID"

# Download job logs and parse PCR measurements
JOB_LOG=$(gh api "repos/${{ github.repository }}/actions/jobs/${JOB_ID}/logs" 2>/dev/null || true)

# The build logs two measurement blocks: the secure image (printed
# first) and the insecure (ALLOW_RECONFIG) image whose PCRs differ.
# The deployed CI enclaves run the insecure EIF, so parse the block
# logged after the insecure image build, not the secure one.
INSECURE_LOG=$(echo "$JOB_LOG" | sed -n '/nitro-enclave-insecure:latest/,$p')

PCR0=$(echo "$INSECURE_LOG" | grep -oP '"PCR0"\s*:\s*"\K[0-9a-f]+' | head -1 || true)
PCR1=$(echo "$INSECURE_LOG" | grep -oP '"PCR1"\s*:\s*"\K[0-9a-f]+' | head -1 || true)
PCR2=$(echo "$INSECURE_LOG" | grep -oP '"PCR2"\s*:\s*"\K[0-9a-f]+' | head -1 || true)

if [ -z "$PCR0" ] || [ -z "$PCR1" ] || [ -z "$PCR2" ]; then
echo "::error::Could not parse PCR measurements from job $JOB_ID logs"
exit 1
# Extract the workflow run ID from the release notes URL
RUN_ID=$(echo "$RELEASE_BODY" | grep -oP 'actions/runs/\K[0-9]+' || true)
if [ -z "$RUN_ID" ]; then
echo "::error::Could not find workflow run ID in release notes for ${{ env.LEGACY_ENCLAVE_RELEASE }}"
exit 1
fi
echo "Found workflow run ID: $RUN_ID"

# Get the build-enclave-and-plugin job ID. The build is now a matrix
# across enclave apps, so match the confidential-http job (which builds
# the enclave the CI test connects to). Fall back to the pre-matrix
# single-job name for older releases.
JOB_ID=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs" \
--jq '.jobs[] | select(.name == "build-enclave-and-plugin" or (.name | startswith("build-enclave-and-plugin (confidential-http"))) | .id' | head -1)
echo "Found job ID: $JOB_ID"

# Download job logs and parse PCR measurements
JOB_LOG=$(gh api "repos/${{ github.repository }}/actions/jobs/${JOB_ID}/logs" 2>/dev/null || true)

# The build logs two measurement blocks: the secure image (printed
# first) and the insecure (ALLOW_RECONFIG) image whose PCRs differ.
# The deployed CI enclaves run the insecure EIF, so parse the block
# logged after the insecure image build, not the secure one.
INSECURE_LOG=$(echo "$JOB_LOG" | sed -n '/nitro-enclave-insecure:latest/,$p')

PCR0=$(echo "$INSECURE_LOG" | grep -oP '"PCR0"\s*:\s*"\K[0-9a-f]+' | head -1 || true)
PCR1=$(echo "$INSECURE_LOG" | grep -oP '"PCR1"\s*:\s*"\K[0-9a-f]+' | head -1 || true)
PCR2=$(echo "$INSECURE_LOG" | grep -oP '"PCR2"\s*:\s*"\K[0-9a-f]+' | head -1 || true)

if [ -z "$PCR0" ] || [ -z "$PCR1" ] || [ -z "$PCR2" ]; then
echo "::error::Could not parse PCR measurements from job $JOB_ID logs"
exit 1
fi
fi

# Write pcr_measurements.json matching the format produced by nitro-cli
Expand Down Expand Up @@ -912,8 +927,9 @@ jobs:
# Clean up JD database volume if it exists
docker volume rm job-distributor-db-volume || true

# Scheduled/release runs (and the backwards-compat variant) exercise REAL
# enclaves and only run the ./tests module (integration + tests/e2e).
# Real-enclave runs (the nightly schedule, release pushes, and the
# label-triggered variant) only run the ./tests module (integration +
# tests/e2e).
if [ "${{ matrix.real }}" = "true" ]; then
echo "Real-enclave run: testing the ./tests module against Nitro enclaves"
# Leave ENCLAVE_TYPE unset so the harness provisions real Nitro enclaves.
Expand Down
44 changes: 44 additions & 0 deletions enclave-client/attestation-validator/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package attestationvalidator

import (
"fmt"

"github.com/fxamacker/cbor/v2"
)

// coseSign1 mirrors the COSE_Sign1 array structure wrapping a Nitro attestation.
type coseSign1 struct {
_ struct{} `cbor:",toarray"` //nolint:revive // idiomatic CBOR array encoding
Protected []byte
Unprotected cbor.RawMessage
Payload []byte
Signature []byte
}

// attestationPayload holds the Nitro attestation payload fields used for diagnostics.
type attestationPayload struct {
ModuleID string `cbor:"module_id"`
PCRs map[uint][]byte `cbor:"pcrs"`
}

// DescribeMeasurements decodes the PCR measurements a Nitro attestation actually
// reports, for diagnostic logging when validation fails. It does NOT verify the
// signature or certificate chain; it only parses the COSE_Sign1 payload so the
// measurements the enclave reported can be compared against the expected trusted
// values. Returns a short summary, or a note when the document cannot be parsed
// (e.g. a fake attestation).
func DescribeMeasurements(attestation []byte) string {
if len(attestation) == 0 {
return "no attestation received"
}
var sign1 coseSign1
if err := cbor.Unmarshal(attestation, &sign1); err != nil || len(sign1.Payload) == 0 {
return "unparseable attestation (not a Nitro COSE document)"
}
var payload attestationPayload
if err := cbor.Unmarshal(sign1.Payload, &payload); err != nil {
return "unparseable attestation payload"
}
return fmt.Sprintf("moduleID=%s PCR0=%x PCR1=%x PCR2=%x",
payload.ModuleID, payload.PCRs[0], payload.PCRs[1], payload.PCRs[2])
}
2 changes: 1 addition & 1 deletion enclave-client/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.26.4
replace github.com/smartcontractkit/confidential-compute => ../

require (
github.com/fxamacker/cbor/v2 v2.9.0
github.com/smartcontractkit/chainlink-common v0.11.2-0.20260714160921-4033d0253977
github.com/smartcontractkit/confidential-compute v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.11.1
Expand All @@ -19,7 +20,6 @@ require (
github.com/cloudevents/sdk-go/v2 v2.16.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/doyensec/safeurl v0.2.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand Down
3 changes: 2 additions & 1 deletion enclave-client/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,8 @@ func (c *enclavePool) validateAttestationAgainstMultipleMeasurements(enclave typ
if combinedErr == nil {
combinedErr = errors.New("no trusted measurements configured")
}
return false, fmt.Errorf("attestation validation failed for enclave %x: %w", enclave.EnclaveID, combinedErr)
return false, fmt.Errorf("attestation validation failed for enclave %x (received %s): %w",
enclave.EnclaveID, attestationvalidator.DescribeMeasurements(attestation), combinedErr)
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions scripts/check_prod.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,41 @@ import (
"os"
"time"

"github.com/fxamacker/cbor/v2"

"github.com/smartcontractkit/chainlink-common/pkg/teeattestation/nitro"
)

// coseSign1 mirrors the COSE_Sign1 array structure wrapping a Nitro attestation.
type coseSign1 struct {
_ struct{} `cbor:",toarray"`
Protected []byte
Unprotected cbor.RawMessage
Payload []byte
Signature []byte
}

// attestationPayload holds the Nitro attestation payload fields used for diagnostics.
type attestationPayload struct {
ModuleID string `cbor:"module_id"`
PCRs map[uint][]byte `cbor:"pcrs"`
}

// parseAttestationPCRs decodes the PCR measurements a Nitro attestation reports,
// without verifying its signature or certificate chain. It lets us surface the
// measurements the enclave actually reported when no known measurement validates.
func parseAttestationPCRs(attestation []byte) (map[uint][]byte, error) {
var sign1 coseSign1
if err := cbor.Unmarshal(attestation, &sign1); err != nil || len(sign1.Payload) == 0 {
return nil, fmt.Errorf("not a Nitro COSE document")
}
var payload attestationPayload
if err := cbor.Unmarshal(sign1.Payload, &payload); err != nil {
return nil, fmt.Errorf("unparseable attestation payload: %w", err)
}
return payload.PCRs, nil
}

// pcrSetJSON renders a PCRSet as the trusted-measurement JSON that
// nitro.ValidateAndParse consumes (hex-string pcr0/pcr1/pcr2 fields).
func pcrSetJSON(m PCRSet) []byte {
Expand Down Expand Up @@ -64,6 +96,13 @@ func runCheckProd(args []string) {
fmt.Printf(" PCR2: %s\n", hex.EncodeToString(doc.PCRs[2]))
} else {
fmt.Printf(" Running: UNKNOWN - no listed measurement validates the live attestation\n")
if pcrs, err := parseAttestationPCRs(resp.Attestation); err == nil {
fmt.Printf(" Attested PCR0: %s\n", hex.EncodeToString(pcrs[0]))
fmt.Printf(" Attested PCR1: %s\n", hex.EncodeToString(pcrs[1]))
fmt.Printf(" Attested PCR2: %s\n", hex.EncodeToString(pcrs[2]))
} else {
fmt.Printf(" (could not parse attested PCRs: %v)\n", err)
}
// Try each measurement anyway to surface why none matched.
for i, m := range validMeasurements {
_, err := nitro.ValidateAndParse(resp.Attestation, userData[:], pcrSetJSON(m))
Expand Down
2 changes: 1 addition & 1 deletion scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.26.4
replace github.com/smartcontractkit/confidential-compute => ../

require (
github.com/fxamacker/cbor/v2 v2.9.0
github.com/smartcontractkit/chainlink-common v0.11.2-0.20260714160921-4033d0253977
github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260622152157-c8e129347b8b
github.com/smartcontractkit/confidential-compute v0.0.0-20251014091702-53f02d33c6b4
Expand All @@ -16,7 +17,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/doyensec/safeurl v0.2.2 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
Expand Down
Loading