Verification & Measurement
Release signatures, runtime attestation, MRTD/RTMR enforcement, and operator verification paths
What Gets Released
Every release produces artifacts in exactly two families. Each family lives on its own GitHub release tag and carries its own signatures, checksums, and SBOM.
KMS Family
Tag pattern: mero-kms-vX.Y.Z
Contains the mero-kms-phala Docker image, attestation policy JSON, compose hash file, SBOM, SHA-256 checksums, and Sigstore signatures. Built by release-kms-phala.yaml.
Artifacts:
- Docker image (GHCR)
- attestation-policy-{profile}.json per profile
- published-mrtds.json
- compose-hash.txt
- checksums.sha256
- Sigstore bundle (.sigstore.json)
- SBOM (sbom.spdx.json)
Node-Image Family
Tag pattern: mero-tee-vX.Y.Z
Contains the GCP TDX confidential VM image built by Packer + Ansible, plus the published MRTD/RTMR reference values, checksums, and Sigstore signatures. Built by release-node-image-gcp.yaml.
Artifacts:
- GCP VM image (per profile)
- published-mrtds.json
- checksums.sha256
- Sigstore bundle (.sigstore.json)
- SBOM (sbom.spdx.json)
- Auto-generated release notes
Release Signatures
Every release asset is signed with Sigstore keyless signing (Fulcio + Rekor). Signatures prove two things — and critically fail to prove a third.
Proves: Provenance
The artifact was produced by a specific GitHub Actions workflow run, from a specific commit, in the calimero-network/mero-tee repository. The OIDC identity chain is: GitHub → Fulcio certificate → Rekor transparency log.
Proves: Integrity
The artifact has not been modified since it was signed. Any bit flip in the file invalidates the Sigstore bundle. SHA-256 checksums provide a second independent integrity check.
Does NOT Prove: Runtime State
Signatures say nothing about whether a running instance loaded the signed artifact or whether it was tampered with after deployment. That is the job of runtime attestation (TDX quotes, MRTD/RTMR verification).
Operators must perform both release verification (signatures + checksums) and runtime attestation (TDX quote + measurement comparison) to establish full trust. Neither alone is sufficient.
Verification Matrix
Three actors verify three different things. Each row is a distinct trust relationship with its own protocol.
merod verifies KMS
Protocol: POST /attest with a fresh nonce → receive raw TDX quote → verify quote signature → confirm report_data contains nonce binding → compare MRTD against known-good KMS measurement from config.
When: Every node boot, before requesting any key. This is "Plane 1" in the trust model.
Rejects if: Quote signature invalid, nonce mismatch, MRTD not in expected set, report_data tampered.
KMS verifies merod
Protocol: Node calls POST /challenge → receives nonce → produces TDX quote binding nonce + peerId → calls POST /get-key with quote + signature → KMS verifies signature, quote, and policy.
When: Every key release request. This is "Plane 2" in the trust model.
Rejects if: Signature invalid, challenge expired/consumed, quote invalid, MRTD/RTMR0-3/TCB not in policy allowlist.
Operator verifies releases
Protocol: Download release assets → run verify-release-assets.sh → confirms Sigstore signatures against expected OIDC issuer and workflow identity → validates SHA-256 checksums → inspects published-mrtds.json and compatibility catalog.
When: Before deploying any new release. Before updating attestation policy.
Rejects if: Sigstore verification fails, checksum mismatch, unexpected workflow identity, MRTD values differ from expected.
Profile Compatibility
Node images are built in one of three profiles. Each profile produces distinct RTMR3 values, creating cryptographic trust cohorts. The KMS attestation policy is scoped per-profile — a policy for locked-read-only will never accept a debug node.
Profile-Isolated Trust Cohorts
Each profile writes a unique event to RTMR3 at boot. Because RTMR extension is hardware-enforced and one-way (SHA-384 extend), a node cannot change its RTMR3 after boot. The KMS policy contains a per-profile allowlist.
attestation-policy-debug.json → allowed_rtmr3: ["aabbcc..."]
attestation-policy-debug-read-only.json → allowed_rtmr3: ["ddeeff..."]
attestation-policy-locked-read-only.json → allowed_rtmr3: ["112233..."]
// NEVER mix debug RTMR3 values into a production policy.
// Doing so allows debug nodes to obtain production keys.
Rule: Never add debug profile RTMR3 values to a production KMS policy. Profile isolation is a security boundary, not a convenience flag.
Operator Quick Path
For operators who need to verify a release before deployment. This is the fast path — for full audit, see the "Minimal Download Sets" section below.
Step 1: Run verify-release-assets.sh
$ ./scripts/verify-release-assets.sh mero-kms-v1.2.0
# Verify node-image release
$ ./scripts/verify-release-assets.sh mero-tee-v1.2.0
# The script:
# 1. Downloads release assets from GitHub
# 2. Verifies Sigstore signatures (cosign verify-blob)
# 3. Checks SHA-256 checksums
# 4. Validates OIDC issuer = https://token.actions.githubusercontent.com
# 5. Validates workflow identity matches expected release workflow
# 6. Prints PASS/FAIL for each asset
Step 2: Inspect Compatibility Map
Each release includes a compatibility catalog linking KMS versions to compatible node-image versions. Verify the version pair you intend to deploy is listed.
$ cat compatibility-catalog.json | jq '.entries[] | select(.kms == "1.2.0")'
# Confirms which node-image versions are compatible with this KMS
Step 3: Confirm Profile & Policy URL
Before deploying, verify the KMS is configured with the correct profile's policy URL:
MERO_KMS_POLICY_URL=https://github.com/calimero-network/mero-tee/releases/download/mero-kms-v1.2.0/attestation-policy-locked-read-only.json
# Optionally pin by hash:
MERO_KMS_POLICY_SHA256=e3b0c44298fc1c149afbf4c8996fb924...
Client Attestation Config
Nodes need attestation configuration to know which KMS measurements to expect (Plane 1 verification). Two scripts handle generation and application.
generate-merod-kms-phala-attestation-config.sh
$ ./scripts/generate-merod-kms-phala-attestation-config.sh \
--kms-version 1.2.0 \
--profile locked-read-only \
--output merod-attestation.json
# The script:
# 1. Fetches published-mrtds.json from the KMS release
# 2. Extracts MRTD for the given KMS version
# 3. Generates a config block with expected_mrtd, kms_url, nonce_binding
# 4. Writes JSON config suitable for merod's --attestation-config flag
Applying Config
$ ./scripts/apply-merod-attestation-config.sh \
--config merod-attestation.json \
--node-data-dir /var/lib/merod
# Or pass at boot:
$ merod run --attestation-config merod-attestation.json
The generated config ties the node to a specific KMS release. When the KMS is upgraded, re-run the generation script with the new version and redeploy the config.
Compose Hash
The compose hash proves the Docker Compose configuration used to run the KMS has not been modified. It is a SHA-256 digest of the normalized docker-compose.yml file.
What It Proves
The compose hash binds the KMS deployment configuration (image tag, environment variables, volume mounts, network config) to a specific hash. If any field in the compose file changes, the hash changes.
Trust Path
Trusted: From Verified Attestation
The compose hash is meaningful only when obtained through the verified attestation path. The KMS embeds the compose hash in its TDX quote report_data. A verified TDX quote proves the compose file was loaded at boot time.
NOT Trusted: From Provisioning Metadata
Do not trust compose hash values obtained from instance metadata, operator-provided config, or any non-attested source. Only the TDX-attested value is authoritative.
Extracting from Release Assets
$ cat compose-hash.txt
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# Compare against the value in a verified TDX quote:
# 1. Obtain quote via POST /attest
# 2. Parse report_data to extract compose hash
# 3. Compare: quote_compose_hash == release_compose_hash
If the values differ, the running KMS is not using the expected configuration. Investigate before trusting key releases from that instance.
MRTD / RTMR Verification
The core measurement verification flow: compare observed values from a verified TDX quote against the reference values published in published-mrtds.json for a given release + profile combination.
Step-by-Step
Get Observed Values
Obtain a TDX quote from the target instance via POST /attest (for KMS) or from the node's local TDX guest driver. Parse the quote to extract MRTD, RTMR0, RTMR1, RTMR2, RTMR3.
Verify the Quote
Before trusting any values, verify the TDX quote cryptographic signature. Either verify locally or submit to Intel Trust Authority. An unverified quote is meaningless.
Fetch Reference Values
Download published-mrtds.json from the appropriate GitHub release tag. For KMS: mero-kms-vX.Y.Z. For node images: mero-tee-vX.Y.Z.
$ cat published-mrtds.json | jq '.'
Compare by Release + Profile
Look up the entry in published-mrtds.json matching your target release version and profile. Compare each register:
observed.rtmr0 == published[version][profile].rtmr0 // firmware
observed.rtmr1 == published[version][profile].rtmr1 // kernel
observed.rtmr2 == published[version][profile].rtmr2 // application
observed.rtmr3 == published[version][profile].rtmr3 // profile event
Full Policy Tuple Enforcement
For KMS policy verification, all five registers plus the TCB status must match the policy. The full tuple is: (mrtd, rtmr0, rtmr1, rtmr2, rtmr3, tcb_status). Any single mismatch is a policy violation.
Why RTMRs May Match Across Profiles
It is normal for some RTMR values to be identical between profiles of the same release. The key discriminator is RTMR3.
MRTD
Changes when the VM image changes. Different images → different MRTD. Within the same release, MRTD may differ per profile if the image differs, or match if the base image is shared.
RTMR0 / RTMR1
Measure firmware and kernel. If all profiles use the same firmware and kernel (common), RTMR0 and RTMR1 will be identical across profiles. This is expected — profile differentiation is not their job.
RTMR2
Measures the application layer. May diverge across profiles if the application config or binary differs, or match if the same app is loaded with different runtime params.
RTMR3 (Discriminator)
Always differs between profiles within the same release. Each profile writes a unique boot event to RTMR3. This is the definitive profile separator. If RTMR3 matches the policy allowlist, the profile is confirmed.
RTMR0 Troubleshooting
A common operator issue: the RTMR0 value published in GitHub release assets does not match the RTMR0 observed on a live node. Here's why and how to fix it.
Why GitHub Expected ≠ Live Node
Different Image / Boot Chain
The live node booted a different VM image than the one from the release. This can happen if the cloud provider updated the base image, the image was rebuilt with a different firmware version, or the wrong image family was selected.
Stale Release Asset
The published-mrtds.json in the release was generated from a build that used a different firmware than the currently deployed image. This can happen if the image was rebuilt without cutting a new release.
How to Fix
Confirm Image Provenance
Verify the live node is running the exact image from the release. Check the GCP image name, family, and project against the release notes.
# Compare against image name in release notes
Check Firmware Version
If the image matches but RTMR0 differs, the firmware (TDVF) may have been updated by the cloud provider. Compare the firmware hash in the quote against the expected value.
Re-Measure or Re-Release
If the deployed image is correct but measurements drifted (firmware update), either: (a) rebuild and re-release the image to pick up new measurements, or (b) update the attestation policy to include the new RTMR0 value after verifying the firmware change is legitimate.
RTMR3 Image Legitimacy
RTMR3 is the per-profile runtime measurement register. Understanding how it's generated is critical for verifying image legitimacy and detecting binary-swap attacks.
Generation Formula
RTMR3_new = SHA384(RTMR3_old || event_data)
// Initial state (before any extensions):
RTMR3_initial = 0x000...000 // 48 bytes of zeros
// At boot, the image writes a profile-specific event:
event_data = SHA384("mero-tee:profile:{profile_name}")
// Final RTMR3 (single extension from zero):
RTMR3 = SHA384(0x000...000 || SHA384("mero-tee:profile:locked-read-only"))
Release Artifacts
Each release computes and publishes the expected RTMR3 per profile in published-mrtds.json. The CI pipeline extends from the known initial state using the profile string, so the published value is deterministic and reproducible.
Client Verification Paths
Path A: Compare Against Published
Download published-mrtds.json from the verified release. Look up the RTMR3 for your profile. Compare against the RTMR3 in a verified TDX quote. Match = legitimate.
Path B: Recompute Locally
Compute the expected RTMR3 yourself using the formula above with the profile string. Compare against the quote. This verifies the published value itself is correct.
Binary-Swap Mitigation
An attacker who replaces the merod binary inside the VM changes MRTD (the image hash changes). But could they create a malicious image that extends the same RTMR3? No — RTMR3 is extended during early boot by trusted firmware code before the application binary runs. The application cannot control what gets extended to RTMR3 unless it controls the boot chain, which would change RTMR0/RTMR1.
Safety Improvements
- Multi-register binding: The KMS policy checks all five registers simultaneously. A swap that preserves RTMR3 will fail on MRTD or RTMR2.
- Kernel command-line in RTMR2/3: Some configurations extend the kernel cmdline into RTMRs. Changing boot parameters (to disable security) changes RTMR values.
- Profile event is early-boot: The profile string is written to RTMR3 during initramfs, before any user-space code executes.
Minimal Download Sets
You don't need every release asset to verify. Two modes: quick verify (operator confidence) and full audit (complete reproducibility check).
KMS Family
Quick Verify
- checksums.sha256
- checksums.sha256.sigstore.json
- published-mrtds.json
Full Audit
- All of the above, plus:
- attestation-policy-{profile}.json
- compose-hash.txt
- sbom.spdx.json
- Docker image digest verification
Node-Image Family
Quick Verify
- checksums.sha256
- checksums.sha256.sigstore.json
- published-mrtds.json
Full Audit
- All of the above, plus:
- sbom.spdx.json
- GCP image metadata verification
- Packer manifest comparison
Verification Examples
Expected script output and common failure patterns. Expand each section for detailed examples.
KMS Release Verification — Expected Output
[INFO] Downloading release assets for mero-kms-v1.2.0...
[INFO] Verifying Sigstore signature for checksums.sha256...
[PASS] Sigstore signature valid
Issuer: https://token.actions.githubusercontent.com
Workflow: .github/workflows/release-kms-phala.yaml
Repository: calimero-network/mero-tee
[INFO] Verifying SHA-256 checksums...
[PASS] attestation-policy-locked-read-only.json: OK
[PASS] published-mrtds.json: OK
[PASS] compose-hash.txt: OK
[PASS] sbom.spdx.json: OK
✓ All 4 assets verified successfully
Node-Image Release Verification — Expected Output
[INFO] Downloading release assets for mero-tee-v1.2.0...
[INFO] Verifying Sigstore signature for checksums.sha256...
[PASS] Sigstore signature valid
Issuer: https://token.actions.githubusercontent.com
Workflow: .github/workflows/release-node-image-gcp.yaml
Repository: calimero-network/mero-tee
[INFO] Verifying SHA-256 checksums...
[PASS] published-mrtds.json: OK
[PASS] sbom.spdx.json: OK
✓ All 2 assets verified successfully
Common Failure Patterns
Sigstore Verification Failed
Error: certificate identity mismatch
Expected: .github/workflows/release-kms-phala.yaml
Got: .github/workflows/build-dev.yaml
The asset was signed by a different workflow than expected. This could indicate a tampered release or an incorrect release tag. Do not deploy.
Checksum Mismatch
Expected: a1b2c3d4...
Got: e5f6a7b8...
The downloaded file differs from the signed checksum. Re-download the asset. If it persists, the release may have been tampered with.
OIDC Issuer Mismatch
Expected: https://token.actions.githubusercontent.com
Got: https://other-issuer.example.com
The certificate was not issued by GitHub Actions. This is a critical failure — the signing identity cannot be trusted.
Missing Assets
- checksums.sha256.sigstore.json
- published-mrtds.json
The release is incomplete. Do not deploy. Check if the release workflow completed successfully.
Operator Troubleshooting Checklist
- Verify you have the right release tag. KMS uses mero-kms-vX.Y.Z, node images use mero-tee-vX.Y.Z. Mixing them up is a common mistake.
- Check cosign is installed. The verification script requires cosign v2+. Run cosign version to verify.
- Check network access. Sigstore verification requires access to rekor.sigstore.dev and fulcio.sigstore.dev. Ensure these are not blocked by firewall rules.
- Re-download assets. Partial downloads or network corruption can cause checksum failures. Delete and re-download.
- Inspect the Sigstore bundle. If signature verification fails, inspect the .sigstore.json file to check the embedded certificate and verify the signing time.
- Compare MRTD values. If measurements don't match, compare published-mrtds.json across releases to see when the value changed.
- Check compatibility catalog. Ensure the KMS + node-image version pair is listed as compatible before diagnosing measurement mismatches.
Workflow Identity
Release signatures embed the GitHub Actions workflow identity. This is the strongest anchor tying a release to its source. Operators should verify the workflow identity matches expectations.
KMS Releases
Repository: calimero-network/mero-tee
OIDC Issuer: https://token.actions.githubusercontent.com
Trigger: release tag matching mero-kms-v*
Node-Image Releases
Repository: calimero-network/mero-tee
OIDC Issuer: https://token.actions.githubusercontent.com
Trigger: release tag matching mero-tee-v*
Any release signed by a different workflow or repository is not legitimate, even if the Sigstore signature is technically valid. The verify-release-assets.sh script checks this automatically.