Kaskad

Verify the Enclaves

The Nuntius oracle and the Pontifex bridge sign with keys that exist only inside AWS Nitro Enclaves. You can check what runs there yourself. On Robinhood Chain, four checks connect the published source code to the signatures the contracts accept:

CheckQuestion it answersTooling
1. RebuildDoes the published source produce the measurements (PCR0, PCR1, PCR2) that are pinned on-chain?Docker, nitro-cli
2. AttestIs a real Nitro enclave running an image with those measurements right now, and which key does it hold?Python
3. PinDoes the contract only admit signers attested for that image?cast
4. SignersIs the key from the attestation the only one the contract accepts?cast
Public repository
Source code
Check 1. Reproducible build
Image measurements
PCR0 · PCR1 · PCR2
Check 2. AWS Nitro signs an attestation document
AWS Nitro Enclave
Attestation
  • “An enclave with these PCRs holds this public key”
Public key → Ethereum address
Signer
Ethereum address of the enclave key
Checks 3 and 4. Registered only if PCR0 matches expectedPcr0()
On-chain
Oracle and bridge contracts
  • verify every price update and claim against that signer

Live Values

Robinhood Chain (chain ID 4663). The bridge and the oracle are built from the same public tree, Kaskad-Lending/kaskad-pontifex-enclave.

Oracle (Nuntius)Bridge (Pontifex)
DockerfileDockerfile.oracleDockerfile.pontifex
PCR05248a0d0ca1bea3044a270078286a7f788dfba98ada7069bdc4db81407e6a3a65c5be8f76e3cd299571621d71cc022968761b33a338db4fab12c9201eef87e1c582310d59645848c2574061d1e070d71238976ea1af05ad4b4e645004621aac6
PCR14b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda4934b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493
PCR2cb935649ecdfcff7944cd891ae8de97b91c1fd252faa38547c68eb8a447a0acee3ef3244309d38673df49cefce5c4a46053d5d61b98e960c777ea280b7b9550951d3b105689f23bb63d25a2afaf0ff3823ad240aff39f34437289e2761a4a2c1
Pinned inKaskadPriceOracleV2 0xB4922b744cc395E05Cc962d55FA63FBab0599ec2KskdEntry 0x7b1D080d2485E63791468d73418e2B262c25C402
Signer0xE4D88bE9756EB3fc5bD28B81642605FBF238d2c90x7d3B23a631501a35cafe4F00133653A1b90f2CC3
Attestationhttps://nuntius.kaskad.live/attestationhttps://nuntius.kaskad.live/bridge/attestation

PCR0 measures the whole enclave image, PCR1 the Linux kernel and bootstrap, PCR2 the application. The contracts store the first 32 of the 48 PCR0 bytes.

These values change when an image is upgraded. The live attestation in step 2 shows the image that runs now.

1. Rebuild the Images

You need x86_64 Linux and Docker, but no Nitro hardware and no AWS account. Use the default builder with the docker driver and the classic image store. The repository README notes that the containerd image store and the docker-container BuildKit driver build the same binary but give a different PCR2, and therefore a different PCR0.

check the builder
docker buildx inspect default | grep Driver    # Driver: docker
docker info --format '{{.Driver}}'             # overlay2

The build arguments come from host/eif-config/robinhood-mainnet.env in the repository. They hold only public values, such as contract addresses and RPC URLs, and are part of what PCR0 measures.

build both images
git clone https://github.com/Kaskad-Lending/kaskad-pontifex-enclave && cd kaskad-pontifex-enclave
git checkout 79c81b806232c325b3b4b01c9d47cb8713e71063    # the commit of the live images
( set -a; . host/eif-config/robinhood-mainnet.env; set +a
  docker build --builder default --no-cache -f Dockerfile.pontifex -t pontifex-bridge \
    --build-arg SOURCE_DATE_EPOCH=1700000000 \
    --build-arg PONTIFEX_EXIT --build-arg PONTIFEX_KSKD --build-arg PONTIFEX_ENTRY \
    --build-arg PONTIFEX_CHAIN_ID --build-arg PONTIFEX_IGRA_RPCS \
    --build-arg PONTIFEX_VERSION --build-arg PONTIFEX_ANCESTOR_PCRS .
  docker build --builder default --no-cache -f Dockerfile.oracle -t oracle-enclave \
    --build-arg SOURCE_DATE_EPOCH=1700000000 \
    --build-arg KEYEX_ORACLE_REGISTRY --build-arg KEYEX_ORACLE_RH_RPCS --build-arg KEYEX_ORACLE_SAFE \
    --build-arg KEYEX_ORACLE_OWNERS --build-arg KEYEX_ORACLE_THRESHOLD --build-arg KEYEX_ORACLE_CHAIN_ID \
    --build-arg KEYEX_ORACLE_VERSION --build-arg KEYEX_ORACLE_ANCESTORS --build-arg KEYEX_ORACLE_PEERS . )

Measure both images with nitro-cli 1.5.0 from the Amazon Linux 2023 repository. Each command prints a JSON object with PCR0, PCR1 and PCR2:

measure the images
mkdir -p out
docker run --rm --security-opt label=disable \
  -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD/out:/out" \
  -e NITRO_CLI_ARTIFACTS=/tmp/nca public.ecr.aws/amazonlinux/amazonlinux:2023 bash -c '
    set -e
    dnf install -y aws-nitro-enclaves-cli-1.5.0-0.amzn2023 aws-nitro-enclaves-cli-devel-1.5.0-0.amzn2023 >/dev/null
    mkdir -p /var/log/nitro_enclaves /tmp/nca
    nitro-cli build-enclave --docker-uri pontifex-bridge --output-file /out/pontifex.eif
    nitro-cli build-enclave --docker-uri oracle-enclave --output-file /out/oracle.eif'
output (bridge)
{
  "Measurements": {
    "HashAlgorithm": "Sha512_48 { .. }",
    "PCR0": "8761b33a338db4fab12c9201eef87e1c582310d59645848c2574061d1e070d71238976ea1af05ad4b4e645004621aac6",
    "PCR1": "4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493",
    "PCR2": "053d5d61b98e960c777ea280b7b9550951d3b105689f23bb63d25a2afaf0ff3823ad240aff39f34437289e2761a4a2c1"
  }
}
output (oracle)
{
  "Measurements": {
    "HashAlgorithm": "Sha512_48 { .. }",
    "PCR0": "5248a0d0ca1bea3044a270078286a7f788dfba98ada7069bdc4db81407e6a3a65c5be8f76e3cd299571621d71cc02296",
    "PCR1": "4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493",
    "PCR2": "cb935649ecdfcff7944cd891ae8de97b91c1fd252faa38547c68eb8a447a0acee3ef3244309d38673df49cefce5c4a46"
  }
}

All three PCRs of each image must equal the live values. On 21 October 2026 this recipe, run on commit 79c81b8, reproduced all three PCRs of both images exactly.

Compare PCRs, not the hash of the .eif file. The EIF records its build time outside the measured sections, so the file hash changes on every build while the PCRs do not.

2. Verify the Live Attestation

Each enclave publishes a Nitro attestation document: a COSE_Sign1 structure signed by the Nitro hypervisor through a certificate chain that ends at the AWS Nitro Enclaves root certificate. It carries the PCRs of the running image and the enclave's secp256k1 public key.

The script below fetches a document, checks the certificate chain against the root fingerprint published by AWS, checks the COSE signature, and derives the Ethereum address of the enclave key.

verify_attestation.py
"""Verify a Nitro attestation from a Kaskad enclave endpoint; print PCRs and the signer address."""
import datetime
import hashlib
import json
import sys
import urllib.request
from urllib.parse import parse_qs, urlparse

import cbor2
from Crypto.Hash import keccak
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature

# SHA-256 of the AWS Nitro Enclaves root certificate, as published by AWS:
# https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html
AWS_NITRO_ROOT_SHA256 = "641a0321a3e244efe456463195d606317ed7cdcc3c1756e09893f3c68f79bb5b"
COSE_ALG_ES384 = -35


def fail(msg: str) -> None:
    sys.exit(f"FAIL: {msg}")


url = sys.argv[1] if len(sys.argv) > 1 else "https://nuntius.kaskad.live/attestation"
# The bridge endpoint signs a caller-chosen ?nonce= into the document, which proves it is fresh.
expected_nonce = parse_qs(urlparse(url).query).get("nonce", [None])[0]
req = urllib.request.Request(url, headers={"User-Agent": "nuntius-verify/1"})
body = json.load(urllib.request.urlopen(req, timeout=20))
# The oracle serves "attestation_doc", the bridge serves "attestation".
doc = bytes.fromhex((body.get("attestation_doc") or body["attestation"]).removeprefix("0x"))

cose = cbor2.loads(doc)
protected, _unprotected, payload, signature = cose.value if isinstance(cose, cbor2.CBORTag) else cose
if cbor2.loads(protected).get(1) != COSE_ALG_ES384:
    fail("attestation is not signed with ES384")
att = cbor2.loads(payload)
signed_at = datetime.datetime.fromtimestamp(att["timestamp"] / 1000, datetime.timezone.utc)

# 1. Certificate chain: AWS root -> intermediates -> enclave certificate.
if hashlib.sha256(att["cabundle"][0]).hexdigest() != AWS_NITRO_ROOT_SHA256:
    fail("chain does not start at the AWS Nitro root")
chain = [x509.load_der_x509_certificate(c) for c in att["cabundle"]]
leaf = x509.load_der_x509_certificate(att["certificate"])
for parent, child in zip(chain, chain[1:] + [leaf]):
    try:
        parent.public_key().verify(child.signature, child.tbs_certificate_bytes,
                                   ec.ECDSA(child.signature_hash_algorithm))
    except InvalidSignature:
        fail(f"broken certificate chain at {child.subject.rfc4514_string()}")
    if not child.not_valid_before_utc <= signed_at <= child.not_valid_after_utc:
        fail(f"certificate not valid at {signed_at}")

# 2. COSE_Sign1 signature over the document, by the enclave certificate.
sig_structure = cbor2.dumps(["Signature1", protected, b"", payload])
r, s = int.from_bytes(signature[:48], "big"), int.from_bytes(signature[48:], "big")
try:
    leaf.public_key().verify(encode_dss_signature(r, s), sig_structure, ec.ECDSA(hashes.SHA384()))
except InvalidSignature:
    fail("document signature does not verify")

# 3. The enclave's secp256k1 public key -> Ethereum address.
pub = att["public_key"]
if len(pub) != 65 or pub[0] != 4:
    fail("public_key is not an uncompressed secp256k1 key")
address = "0x" + keccak.new(digest_bits=256, data=pub[1:]).hexdigest()[-40:]

# 4. Freshness, when a nonce was requested.
nonce = (att.get("nonce") or b"").hex()
if expected_nonce is not None and nonce != expected_nonce.lower():
    fail("document does not carry the requested nonce")

print("signed at       ", signed_at.isoformat())
for i in (0, 1, 2):
    print(f"PCR{i}            ", att["pcrs"][i].hex())
print("PCR0 first 32 B  0x" + att["pcrs"][0][:32].hex())
print("signer address  ", address)
print("matches API     ", address == body["signer"].lower())
print("nonce           ", nonce or "none")
run it
pip install cbor2 cryptography pycryptodome
python verify_attestation.py                                       # oracle
python verify_attestation.py "https://nuntius.kaskad.live/bridge/attestation?nonce=$(openssl rand -hex 32)"
output (oracle)
signed at        2026-10-21T11:57:34.924000+00:00
PCR0             5248a0d0ca1bea3044a270078286a7f788dfba98ada7069bdc4db81407e6a3a65c5be8f76e3cd299571621d71cc02296
PCR1             4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493
PCR2             cb935649ecdfcff7944cd891ae8de97b91c1fd252faa38547c68eb8a447a0acee3ef3244309d38673df49cefce5c4a46
PCR0 first 32 B  0x5248a0d0ca1bea3044a270078286a7f788dfba98ada7069bdc4db81407e6a3a6
signer address   0xe4d88be9756eb3fc5bd28b81642605fbf238d2c9
matches API      True
nonce            none
output (bridge)
signed at        2026-10-21T11:57:36.736000+00:00
PCR0             8761b33a338db4fab12c9201eef87e1c582310d59645848c2574061d1e070d71238976ea1af05ad4b4e645004621aac6
PCR1             4b4d5b3661b3efc12920900c80e126e4ce783c522de6c02a2a5bf7af3a2b9327b86776f188e4be1c1c404a129dbda493
PCR2             053d5d61b98e960c777ea280b7b9550951d3b105689f23bb63d25a2afaf0ff3823ad240aff39f34437289e2761a4a2c1
PCR0 first 32 B  0x8761b33a338db4fab12c9201eef87e1c582310d59645848c2574061d1e070d71
signer address   0x7d3b23a631501a35cafe4f00133653a1b90f2cc3
matches API      True
nonce            8e8ef8654ee699db8fadca22ae91bdaea95696cbecf074c1f8dd8b135f14e0c2
  • Freshness. The bridge endpoint signs a nonce you choose into the document, which proves the document was produced after you chose it. Use a new random nonce for each request. The oracle endpoint does not take a nonce and serves a document cached for up to five minutes, so check its signed at time instead.
  • Tampering. The signature covers the PCRs, the public key and the nonce. Flipping a single bit of PCR0 in a saved document makes the script stop with FAIL: document signature does not verify.

3. Compare PCR0 With the On-chain Pin

expectedPcr0
RH=https://rpc.mainnet.chain.robinhood.com
cast call 0xB4922b744cc395E05Cc962d55FA63FBab0599ec2 "expectedPcr0()(bytes32)" --rpc-url $RH   # oracle
cast call 0x7b1D080d2485E63791468d73418e2B262c25C402 "expectedPcr0()(bytes32)" --rpc-url $RH   # bridge
0x5248a0d0ca1bea3044a270078286a7f788dfba98ada7069bdc4db81407e6a3a6
0x8761b33a338db4fab12c9201eef87e1c582310d59645848c2574061d1e070d71

Each value equals the first 32 bytes of the PCR0 from steps 1 and 2.

4. Confirm the Signers

signers
cast call 0xB4922b744cc395E05Cc962d55FA63FBab0599ec2 "signerCount()(uint256)" --rpc-url $RH        # oracle → 1
cast call 0xB4922b744cc395E05Cc962d55FA63FBab0599ec2 "isValidSigner(address)(bool)" \
  0xe4d88be9756eb3fc5bd28b81642605fbf238d2c9 --rpc-url $RH                                    # oracle → true
cast call 0x7b1D080d2485E63791468d73418e2B262c25C402 "signerCount()(uint256)" --rpc-url $RH        # bridge → 1
cast call 0x7b1D080d2485E63791468d73418e2B262c25C402 "validSigner(address)(bool)" \
  0x7d3B23a631501a35cafe4F00133653A1b90f2CC3 --rpc-url $RH                                    # bridge → true

When signerCount() equals the number of signers you verified by attestation, and each of them is valid, the contract accepts no other key.

Igra

For the Igra oracle, run steps 2 to 4:

Igra
python verify_attestation.py https://kaskad.live/enclave/attestation       # US
python verify_attestation.py https://kaskad.live/enclave-eu/attestation    # EU

IGRA=https://rpc.igralabs.com:8545
ORACLE=0xB01993906D14792DC21B7EA75A971D8191AAE402
cast call $ORACLE "expectedPCR0()(bytes32)" --rpc-url $IGRA
cast call $ORACLE "signerCount()(uint256)" --rpc-url $IGRA                                     # 2
cast call $ORACLE "isValidSigner(address)(bool)" 0x544705F5d72E1C6f24bFD7C2E176F2Cd2aA8Dc2A --rpc-url $IGRA   # US → true
cast call $ORACLE "isValidSigner(address)(bool)" 0xD35e55c7C2d10472e6d9E7132c88BEeD347D91A7 --rpc-url $IGRA   # EU → true

Both documents carry PCR0 2e66fc4f74006323675a397ab3c6b6eab68f2ccc36f4cf10bb4d0e5c82b2106c097e828df9d8723d8fd0021fc9bc482f, and expectedPCR0() returns its first 32 bytes. On Igra this value is fixed at deployment.

What This Proves and What It Does Not

It proves that each key these contracts accept was held, when the attestation was signed, by an enclave running the image you checked. On Robinhood Chain that image is built from the public source. It keeps the key in enclave memory and hands it only to other attested enclaves, so the operators of the host machines cannot read it.

It does not prove:

  • That the code is correct. Verification ties signatures to the source; reviewing what the source does is a separate step.
  • Independence from AWS. The root of trust is the AWS Nitro hypervisor and the AWS Nitro root certificate. The checks assume both are sound.
  • Liveness. A host operator can stop or delay an enclave. That halts updates or claims; it cannot forge them.