#!/usr/bin/env python3 """ geodesic_verify.py — check a Geodesic execution certificate. python geodesic_verify.py --certificate cert.json \ --request request.json \ --result result.json WHAT THIS ANSWERS Was this result produced by Geodesic's engines, inside an AWS Nitro Enclave — and is this certificate about the exact request I sent? Read the second half precisely. The certificate NAMES your request, by binding a hash of the bytes you sent. It does not by itself establish that the numbers handed to the engines were derived from those bytes without error: the gateway performs that translation and the enclave does not re-derive it. Where a certificate carries `input_bytes` you can now settle that yourself — this tool proves those bytes are the ones the enclave measured, and you read them to confirm they say what you asked for. THAT LAST STEP IS YOURS AND CANNOT BE AUTOMATED. See the last entry under "Still NOT checked here". WHAT YOU NEED Python 3.9+ and the `cryptography` package. Nothing else, and in particular nothing from Geodesic: this file is deliberately standalone so that verification does not depend on any software we control. Read it. It is short on purpose. pip install cryptography WHAT IS AND IS NOT CHECKED Checked here: * the AWS X.509 chain over the attestation document, up to the AWS Nitro Enclaves root, which is PINNED IN THIS FILE (see NITRO_ROOT_PEM) * the attestation document's own COSE_Sign1 signature, against the leaf certificate AWS issued to that specific enclave * the certificate's Ed25519 signature, against the enclave's public key * that the certificate's fields are the values INSIDE signed_body, the bytes the signature covers. Every comparison below uses those signed values; the JSON fields beside them are an unsigned rendering. (Up to 2026.08.18.2 this file compared against the rendering, so a genuine signature paired with a different result still printed VERIFIED.) * that the key was attested by AWS to live in an enclave, and that the key in the attestation is the key you verified with * that the request you hold hashes to the certificate's client_digest * that the result you hold hashes to the certificate's output_sha256 * that input_bytes, WHERE THE CERTIFICATE CARRIES THEM, hash to the certificate's input_sha256 — that is, that the struct you are shown is the one the enclave measured. THE STRUCT IS THEN DECODED INTO NAMED FIELDS for you to read; see the G3-2 note below for what that does and does not settle, because the difference is the whole point. * that ephemeris_sha256, IF YOU PASS IT, matches the ephemeris the enclave actually measured. That field is gateway-computed and unsigned on its own; this is what chains it to a signature. * that PCR0 is present and is NOT all zeros. An enclave launched with --debug-mode zeroes every PCR, and AWS still issues it a genuine attestation document with a valid chain — so without this check a debug enclave, which measures nothing at all, passes every other test in this file and prints VERIFIED. There is no legitimate all-zero PCR0. * that PCR0 equals the value you pinned, IF you pass --expect-pcr0. Without it PCR0 is reported but compared against nothing; see below. THE ONE SUBTLETY THAT MATTERS, BECAUSE IT LOOKS LIKE A BUG The chain is validated as of the attestation document's OWN TIMESTAMP, not as of now. AWS issues an enclave's leaf certificate with roughly a three-hour validity window, while the execution certificates that key signs stay meaningful indefinitely. Checking the chain against the current clock would therefore reject every certificate more than a few hours old — turning correct, verifiable evidence into a failure, which is far worse than not checking at all. What a validated chain proves is that AWS vouched for this enclave AT THE MOMENT OF ATTESTATION. That is the claim, and it does not expire. Still NOT checked here: * certificate revocation (AWS publishes no CRL or OCSP responder for these short-lived enclave certificates; the three-hour lifetime is the revocation mechanism) * that PCR0 corresponds to any particular source tree. The chain proves the measurement is genuine, not what was measured. --expect-pcr0 checks it against a value YOU chose to trust; it cannot tell you that value was the right one to trust. Obtain it out of band, the same way you obtained the AWS root fingerprint above. * THAT THE INPUT THE ENCLAVE COMPUTED ON MEANS WHAT YOU ASKED FOR. The certificate carries two signed hashes: client_digest, over the bytes you sent, and input_sha256, over what the enclave actually received. The gateway translates between the two and the enclave does not re-derive that translation, so a mistranslation — from compromise, or from an ordinary unit-conversion or defaulting bug — produces a certificate that is entirely genuine about a computation you did not ask for. WHAT CHANGED, AND BE PRECISE ABOUT IT. Certificates that carry `input_bytes` now hand you that struct, and this tool checks it hashes to the enclave's own `input_sha256`. That makes a mistranslation DETECTABLE: decode the bytes with the published `input_encoding` and read whether they say what you meant. It does not make one IMPOSSIBLE, and this tool cannot do the last step for you. A passing hash proves the struct is the one the enclave measured. Only you know what you intended it to say. **A certificate where every check in this file passes, on a struct you never read, tells you nothing about whether your inputs were used.** Operations whose requests are too large to echo carry no `input_bytes` at all, and for those the gap is exactly as it always was. The tool says which case you are in rather than leaving you to infer it. Pass --print-chain to dump the chain for inspection with tooling you trust. CAPTURE IT ONCE AND YOU NEVER NEED US AGAIN An attestation document carries its OWN AWS certificate chain, and the root that anchors it is pinned in this file. So everything needed to verify a certificate can be held by you, and re-checked with no network access and no Geodesic service running: # once, while the certificate is fresh python geodesic_verify.py --certificate cert.json --export-bundle evidence.json # any time afterwards, offline, with no request to us python geodesic_verify.py --offline evidence.json Retention of the signing keys is GUARANTEED for 90 days from issuance and is in practice indefinite — the register is append-only and cannot be pruned. Capture inside 60 days anyway. The guarantee exists so that you do not have to depend on it, and a bundle you hold is worth more than a promise we make. KEEP THE VERIFIER ALONGSIDE THE BUNDLE. The bundle records which version captured it. Certificate formats change; every published version of this file stays downloadable, so evidence never outlives the tool that reads it. EXIT STATUS 0 every check passed 1 a check failed — the details are printed, and none of them are cosmetic 2 could not run the checks (bad input, missing file, unreachable API) """ from __future__ import annotations import argparse import hashlib import json import struct import sys import urllib.request DEFAULT_API = "https://api.geodesicspacesystems.com" CERT_VERSION_SUPPORTED = 2 #: THE BYTES THE SIGNATURE COVERS, certificate version 2: version, opcode, key #: id, then the client, input, output and engine SHA-256 digests, then the #: enclave's clock. The JSON fields beside signed_body are an UNSIGNED rendering #: of these values, so every comparison in this file reads this struct instead. #: Duplicated from the gateway's CERT_BODY on purpose, like the layouts below. SIGNED_BODY = struct.Struct(" (struct format, [(field name, count, unit or "")]) _FIXED_LAYOUTS = { "CONJUNCTION": ("<16d", [ ("dr_eci", 3, "km"), ("dv_eci", 3, "km/s"), ("combined_covariance 3x3", 9, "km^2"), ("hard_body_radius_km", 1, "km")]), "CAM": ("<32d2I", [ ("primary state", 6, "km, km/s"), ("secondary state", 6, "km, km/s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("jd_tdb_int", 1, ""), ("jd_tdb_frac", 1, ""), ("tca_seconds", 1, "s"), ("burn_seconds", 1, "s"), ("combined_covariance 3x3", 9, "km^2"), ("hard_body_radius_km", 1, "km"), ("pc_target", 1, ""), ("exec_error_fraction", 1, ""), ("mass_kg", 1, "kg"), ("isp_s", 1, "s"), ("zonal_max", 1, ""), ("include_relativity", 1, "")]), "ASSESS": ("<93d3I", [ ("primary state", 6, "km, km/s"), ("secondary state", 6, "km, km/s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("jd_tdb_int", 1, ""), ("jd_tdb_frac", 1, ""), ("window_seconds", 1, "s"), ("sample_seconds", 1, "s"), ("threshold_km", 1, "km"), ("covariance_primary 6x6", 36, "km^2"), ("covariance_secondary 6x6", 36, "km^2"), ("hard_body_radius_km", 1, "km"), ("covariance_step_seconds", 1, "s"), ("zonal_max", 1, ""), ("include_relativity", 1, ""), ("compute_nonlinearity", 1, "")]), "PROPAGATE": ("<15dI", [ ("state", 6, "km, km/s"), ("dt_seconds", 1, "s"), ("mu", 1, "km^3/s^2"), ("j2", 1, ""), ("body_radius_km", 1, "km"), ("mass_kg", 1, "kg"), ("delta_f_newtons", 3, "N"), ("isp_s", 1, "s"), ("steps", 1, "")]), "PROPAGATE_EX": ("<13d4I", [ ("state", 6, "km, km/s"), ("dt_seconds", 1, "s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("delta_f_newtons", 3, "N"), ("mass_kg", 1, "kg"), ("zonal_max", 1, ""), ("include_relativity", 1, ""), ("include_frame_drag", 1, ""), ("steps", 1, "")]), "RV2COE": ("<7d", [ ("position", 3, "km"), ("velocity", 3, "km/s"), ("mu", 1, "km^3/s^2")]), "LAMBERT": ("<8dI", [ ("r1", 3, "km"), ("r2", 3, "km"), ("tof_seconds", 1, "s"), ("mu", 1, "km^3/s^2"), ("prograde", 1, "")]), "SPIRAL": ("<4d", [ ("mass_kg", 1, "kg"), ("thrust_mn", 1, "mN"), ("isp_s", 1, "s"), ("target_altitude_km", 1, "km")]), "SPIRAL_EX": ("<6d", [ ("mass_kg", 1, "kg"), ("thrust_mn", 1, "mN"), ("isp_s", 1, "s"), ("initial_altitude_km", 1, "km"), ("target_altitude_km", 1, "km"), ("delta_inclination_deg", 1, "deg")]), "CONSTELLATION": ("<5d3I", [ ("altitude_km", 1, "km"), ("inclination_deg", 1, "deg"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("horizon_years", 1, "yr"), ("total_satellites", 1, ""), ("planes", 1, ""), ("phasing", 1, "")]), "DECAY": ("<11dI", [ ("alt0_km", 1, "km"), ("bc_m2_per_kg", 1, "m^2/kg"), ("bc_uncertainty", 1, ""), ("rho_uncertainty", 1, ""), ("jd_start", 1, ""), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("f107", 1, ""), ("f107_81day_avg", 1, ""), ("ap", 1, ""), ("utc_seconds_of_day", 1, "s"), ("utc_day_of_year", 1, "")]), "SCREEN": ("<19d3I", [ ("state_a", 6, "km, km/s"), ("state_b", 6, "km, km/s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("jd_tdb_int", 1, ""), ("jd_tdb_frac", 1, ""), ("window_seconds", 1, "s"), ("sample_seconds", 1, "s"), ("threshold_km", 1, "km"), ("zonal_max", 1, ""), ("include_relativity", 1, ""), ("max_approaches", 1, "")]), "COVARIANCE": ("<83d5I", [ ("state", 6, "km, km/s"), ("covariance 6x6", 36, "km^2"), ("process_noise_Q 6x6", 36, "km^2"), ("dt_seconds", 1, "s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("jd_tdb_int", 1, ""), ("jd_tdb_frac", 1, ""), ("steps", 1, ""), ("zonal_max", 1, ""), ("include_relativity", 1, ""), ("compute_nonlinearity", 1, ""), ("has_process_noise", 1, "")]), "COV_REALISM": ("<47d3I", [ ("state", 6, "km, km/s"), ("covariance 6x6", 36, "km^2"), ("dt_seconds", 1, "s"), ("mu", 1, "km^3/s^2"), ("body_radius_km", 1, "km"), ("jd_tdb_int", 1, ""), ("jd_tdb_frac", 1, ""), ("steps", 1, ""), ("zonal_max", 1, ""), ("include_relativity", 1, "")]), } #: opcode -> (header format, header fields, row stride, row description) _VARIABLE_LAYOUTS = { "BC_ESTIMATE": ("<3dI", [ ("mu", 1, "km^3/s^2"), ("rho_mean_kg_m3", 1, "kg/m^3"), ("rho_uncertainty", 1, ""), ("n_samples", 1, "")], 104, "13 x f64 [time_s, ref_r xyz km, ref_v xyz km/s, obs_r xyz km, obs_v xyz km/s]"), "MANEUVER": (" 4 else "]")) return f"{body} {unit}".rstrip() def _print_fields(values, fields, full): i = 0 for name, count, unit in fields: chunk = list(values[i:i + count]) i += count note = "" if name in _SENTINEL_FIELDS and chunk == [-1.0]: note = " <- OMITTED in your request; the engine chose" if full and count > 4: print(f" {name:28} ({count} values, {unit})".rstrip()) for j in range(0, count, 6): print(f" [{j:3}] " + ", ".join(repr(v) for v in chunk[j:j + 6])) else: print(f" {name:28} {_fmt(chunk, unit)}{note}") def decode_input_bytes(operation: str, raw: bytes, full: bool = False) -> None: """ Print `input_bytes` as named fields. Best effort and NEVER fatal: an operation this file does not know is reported as unknown rather than guessed at, because a wrong decode is worse than none. """ import struct as _struct if operation in _FIXED_LAYOUTS: fmt, fields = _FIXED_LAYOUTS[operation] if _struct.calcsize(fmt) != len(raw): print(f" NOTE input_bytes is {len(raw)} bytes but this verifier " f"expects {_struct.calcsize(fmt)} for {operation}. The " f"certificate format has moved past this verifier -- fetch " f"the version named in the bundle. NOT decoding.") return print(f"\n input_bytes decoded ({operation}, {len(raw)} bytes):") _print_fields(_struct.unpack(fmt, raw), fields, full) return if operation in _VARIABLE_LAYOUTS: hfmt, hfields, stride, rowdesc = _VARIABLE_LAYOUTS[operation] hsize = _struct.calcsize(hfmt) if len(raw) < hsize: print(f" NOTE input_bytes is shorter than {operation}'s " f"{hsize}-byte header. NOT decoding.") return print(f"\n input_bytes decoded ({operation}, {len(raw)} bytes):") _print_fields(_struct.unpack(hfmt, raw[:hsize]), hfields, full) rest = len(raw) - hsize print(f" {'-- after the header':28} {rest} bytes") print(f" rows are {stride} bytes: {rowdesc}") if operation in ("PROPAGATE_EPHEM", "PROPAGATE_NC"): print(f" sha256 of bytes [{hsize}:] is the value published as " f"ephemeris_sha256") elif rest % stride == 0: print(f" {rest // stride} rows present") else: print(f" {rest // stride} whole rows plus {rest % stride} " f"trailing bytes -- expected for layouts with a tail") return print(f" NOTE this verifier has no layout for '{operation}'. The bytes " f"still hash correctly; use input_encoding on the certificate to " f"decode them by hand.") def check_ephemeris_digest(raw_input: bytes, operation: str, published: str | None) -> tuple[bool, str] | None: """ Cross-check `ephemeris_sha256` against the bytes the enclave measured. The published field is computed by the GATEWAY and carries no signature of its own -- on its own it is a checksum, not evidence. But it is the SHA-256 of `input_bytes` from the end of the fixed header onwards, so when a certificate carries input_bytes the field becomes checkable against something the enclave signed. That is the whole of its value. """ import struct as _struct if not published or operation not in ("PROPAGATE_EPHEM", "PROPAGATE_NC"): return None hsize = _struct.calcsize(_VARIABLE_LAYOUTS[operation][0]) if len(raw_input) <= hsize: return None got = hashlib.sha256(raw_input[hsize:]).hexdigest() return (got == published, f"ephemeris_sha256 matches the ephemeris the enclave measured " f"({got[:16]}...)") # --------------------------------------------------------------------------- # Minimal CBOR reader. # # Duplicated from the gateway rather than imported, and that is the point: a # verifier that imports Geodesic code is a verifier that trusts Geodesic code. # --------------------------------------------------------------------------- class _Break: """The 0xFF stop code. A sentinel, because CBOR null decodes to None.""" __slots__ = () _BREAK = _Break() _INDEF = -1 def _cbor(buf, i=0): b = buf[i]; major, minor = b >> 5, b & 0x1F; i += 1 if minor < 24: arg = minor elif minor == 31: # Indefinite length. Nitro attestation payloads use these, so a reader # that rejects them cannot read a real document. arg = _INDEF else: width = {24: 1, 25: 2, 26: 4, 27: 8}.get(minor) if width is None: raise ValueError("reserved CBOR additional information") arg = int.from_bytes(buf[i:i + width], "big"); i += width if major == 0: return arg, i if major == 1: return -1 - arg, i if major in (2, 3): if arg == _INDEF: parts = bytearray() while buf[i] != 0xFF: chunk, i = _cbor(buf, i) parts += chunk.encode("utf-8") if isinstance(chunk, str) else chunk i += 1 raw = bytes(parts) else: raw = buf[i:i + arg]; i += arg return (raw if major == 2 else raw.decode("utf-8", "replace")), i if major == 4: out = [] if arg == _INDEF: while True: v, i = _cbor(buf, i) if v is _BREAK: break out.append(v) else: for _ in range(arg): v, i = _cbor(buf, i); out.append(v) return out, i if major == 5: out = {} if arg == _INDEF: while True: k, i = _cbor(buf, i) if k is _BREAK: break v, i = _cbor(buf, i); out[k] = v else: for _ in range(arg): k, i = _cbor(buf, i); v, i = _cbor(buf, i); out[k] = v return out, i if major == 6: return _cbor(buf, i) if major == 7: if arg == _INDEF: return _BREAK, i return {20: False, 21: True, 22: None}.get(arg), i raise ValueError(f"unsupported CBOR major type {major}") def cose_parts(doc: bytes): """(protected, unprotected, payload, signature) — the raw COSE_Sign1 items.""" outer, _ = _cbor(doc) if not isinstance(outer, list) or len(outer) != 4: raise ValueError("attestation is not a 4-element COSE_Sign1 array") return outer[0], outer[1], outer[2], outer[3] def attestation_payload(doc: bytes) -> dict: inner, _ = _cbor(cose_parts(doc)[2]) return inner # --------------------------------------------------------------------------- # Minimal CBOR *writer* — only the shapes Sig_structure needs. # # COSE does not sign the document; it signs a canonical structure built from it # (RFC 8152 §4.4). Rebuilding that structure is the only way to check the # signature, so a reader alone is not enough. # --------------------------------------------------------------------------- def _head(major: int, n: int) -> bytes: if n < 24: return bytes([major | n]) if n < 0x100: return bytes([major | 24, n]) if n < 0x10000: return bytes([major | 25]) + n.to_bytes(2, "big") return bytes([major | 26]) + n.to_bytes(4, "big") def sig_structure(protected: bytes, payload: bytes) -> bytes: """Sig_structure = ["Signature1", protected, external_aad, payload].""" label = b"Signature1" return ( b"\x84" + _head(0x60, len(label)) + label + _head(0x40, len(protected)) + protected + _head(0x40, 0) # external_aad: empty + _head(0x40, len(payload)) + payload ) # --------------------------------------------------------------------------- # The trust anchor. # # PINNED HERE ON PURPOSE. Every attestation document carries its own copy of the # root in `cabundle[0]`, and trusting that copy would be circular — a forged # document would simply carry a forged root and validate perfectly against # itself. The anchor has to come from outside the document, which means it has # to be in this file. # # Confirm it yourself, once, out of band: # # curl -sO https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip # unzip -p AWS_NitroEnclaves_Root-G1.zip | openssl x509 -noout -fingerprint -sha256 # # It must print NITRO_ROOT_SHA256 below. If it does not, STOP: either this file # has been tampered with, or AWS has rotated the root — and you want to know # which before you trust anything it validates. # --------------------------------------------------------------------------- NITRO_ROOT_SHA256 = "641a0321a3e244efe456463195d606317ed7cdcc3c1756e09893f3c68f79bb5b" NITRO_ROOT_PEM = b"""-----BEGIN CERTIFICATE----- MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4 MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb 48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N IwLz3/Y= -----END CERTIFICATE-----""" def _sha256_alg(): from cryptography.hazmat.primitives import hashes return hashes.SHA256() def _validity(cert): """(not_before, not_after) as aware UTC, across cryptography versions.""" try: return cert.not_valid_before_utc, cert.not_valid_after_utc except AttributeError: # cryptography < 42 returns naive UTC from datetime import timezone as _tz return (cert.not_valid_before.replace(tzinfo=_tz.utc), cert.not_valid_after.replace(tzinfo=_tz.utc)) def verify_attestation_chain(doc: bytes): """ Validate the AWS chain and the document's own signature. Returns (checks, info) where checks is a list of (ok, label). Raises only on input that cannot be parsed at all — a document that parses but fails to validate must produce FAILED checks, not an exception, so the caller can print every result rather than the first problem. """ from datetime import datetime, timezone 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 protected, _unprotected, payload_raw, signature = cose_parts(doc) payload, _ = _cbor(payload_raw) checks = [] info = {} # The instant the chain is judged against. See the module docstring: this is # the document's own timestamp, not now. ts_ms = payload.get("timestamp") if not isinstance(ts_ms, int): raise ValueError("attestation payload carries no usable timestamp") instant = datetime.fromtimestamp(ts_ms / 1000.0, tz=timezone.utc) info["attested_at"] = instant anchor = x509.load_pem_x509_certificate(NITRO_ROOT_PEM) anchor_fp = anchor.fingerprint(hashes.SHA256()).hex() checks.append((anchor_fp == NITRO_ROOT_SHA256, f"pinned AWS root matches its published fingerprint " f"({anchor_fp[:16]}...)")) bundle = [bytes(c) for c in (payload.get("cabundle") or [])] leaf_der = payload.get("certificate") if not bundle or not leaf_der: raise ValueError("attestation carries no certificate chain") # The document's own root must BE our pinned root. We then build the chain # from OUR copy, so nothing supplied by the document is ever a trust anchor. supplied_root = x509.load_der_x509_certificate(bundle[0]) checks.append((supplied_root.fingerprint(hashes.SHA256()).hex() == NITRO_ROOT_SHA256, "the document's own root is the pinned AWS Nitro root")) chain = [anchor] + [x509.load_der_x509_certificate(d) for d in bundle[1:]] \ + [x509.load_der_x509_certificate(bytes(leaf_der))] info["chain"] = chain # Every certificate must have been valid at the moment of attestation. expired = [] for c in chain: nb, na = _validity(c) if not (nb <= instant <= na): expired.append(c.subject.rfc4514_string()) checks.append((not expired, "every certificate was valid at the time of attestation " f"({instant:%Y-%m-%d %H:%M:%SZ})" + (f" — outside window: {expired}" if expired else ""))) # Each certificate is signed by its predecessor, and each issuer is a CA. broken = [] for parent, child in zip(chain, chain[1:]): if child.issuer != parent.subject: broken.append(f"{child.subject.rfc4514_string()} (issuer mismatch)") continue try: parent.public_key().verify( child.signature, child.tbs_certificate_bytes, ec.ECDSA(child.signature_hash_algorithm), ) except InvalidSignature: broken.append(child.subject.rfc4514_string()) checks.append((not broken, f"the {len(chain) - 1}-link chain to the AWS root verifies" + (f" — broken at: {broken}" if broken else ""))) non_ca = [] for c in chain[:-1]: try: bc = c.extensions.get_extension_for_class(x509.BasicConstraints).value if not bc.ca: non_ca.append(c.subject.rfc4514_string()) except x509.ExtensionNotFound: non_ca.append(c.subject.rfc4514_string() + " (no basicConstraints)") checks.append((not non_ca, "every issuer in the chain is a CA" + (f" — not a CA: {non_ca}" if non_ca else ""))) # Finally the document itself. COSE ES384 carries the signature as raw # r||s; X.509 tooling wants DER, so it has to be re-encoded before use. leaf = chain[-1] half = len(signature) // 2 der_sig = encode_dss_signature( int.from_bytes(signature[:half], "big"), int.from_bytes(signature[half:], "big"), ) try: leaf.public_key().verify( der_sig, sig_structure(protected, payload_raw), ec.ECDSA(hashes.SHA384()) ) checks.append((True, "the attestation document is signed by that AWS leaf certificate")) except InvalidSignature: checks.append((False, "the attestation document is signed by that AWS leaf certificate")) return checks, info def fetch(url: str) -> dict: req = urllib.request.Request(url, headers={"User-Agent": "geodesic-verify/1.0"}) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode()) def sha256_file(path: str) -> str: with open(path, "rb") as fh: return hashlib.sha256(fh.read()).hexdigest() def _resolve_key(key_id: str, api: str, register_url: str): """ Find the signing key, preferring the live API and falling back to the published static register. Two sources rather than one because they fail in DIFFERENT ways and neither failure should be able to stop a customer verifying. The API is authoritative and current, and is only reachable while a compute node is running. The static register is always reachable and is a snapshot, so it can lag a key minted in the last few minutes. Tried in that order; whichever answered is reported, so the verdict never hides where its inputs came from. Returns (key_record, source_description) or (None, what_was_tried). """ tried = [] for label, url in ( ("live API by key id", f"{api}/v1/attestation/{key_id}"), ("live API current key", f"{api}/v1/attestation"), ): try: got = fetch(url) if got.get("key_id") == key_id: return got, label tried.append(f"{label} -> returned {got.get('key_id')!r}") except Exception as exc: tried.append(f"{label} -> {type(exc).__name__}") try: reg = fetch(register_url) keys = reg.get("keys") or [] for k in keys: if k.get("key_id") == key_id: return k, "published static register" tried.append(f"static register -> {len(keys)} keys, none matching") except Exception as exc: tried.append(f"static register -> {type(exc).__name__}") return None, "; ".join(tried) def _utc_now() -> str: """Capture time, for the bundle. Descriptive only — never used as evidence.""" from datetime import datetime, timezone return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") def _self_digest() -> str | None: """SHA-256 of this file, so a bundle records the tool that produced it.""" try: with open(__file__, "rb") as fh: return hashlib.sha256(fh.read()).hexdigest() except Exception: return None def main() -> int: ap = argparse.ArgumentParser(description="Verify a Geodesic execution certificate.") ap.add_argument("--certificate", help="the certificate JSON") ap.add_argument("--request", help="the EXACT request body bytes you sent") ap.add_argument("--result", help="the EXACT result bytes you were given") ap.add_argument("--api", default=DEFAULT_API) ap.add_argument("--register", default=DEFAULT_REGISTER, help="static key register, used when the API cannot be reached") ap.add_argument("--export-bundle", metavar="PATH", help="write a self-contained evidence bundle that can be " "re-verified offline, without Geodesic") ap.add_argument("--offline", metavar="BUNDLE", help="verify from a previously exported bundle. Makes NO " "network requests of any kind.") ap.add_argument("--expect-pcr0", metavar="HEX", help="the PCR0 you have pinned, 96 hex characters. Given, it " "is checked; omitted, PCR0 is only reported.") ap.add_argument("--print-chain", action="store_true", help="dump the AWS certificate chain for external validation") ap.add_argument("--decode-input", action="store_true", help="print EVERY element of input_bytes rather than the " "first few of each array. The summary is printed " "either way; this is for reading a covariance or a " "sample set in full.") ap.add_argument("--ephemeris-sha256", metavar="HEX", help="the ephemeris_sha256 from the response body. Given, it " "is CHECKED against the ephemeris the enclave actually " "measured. That field is computed by the gateway and " "carries no signature of its own, so on its own it is a " "checksum; this is what turns it into evidence.") args = ap.parse_args() if not args.certificate and not args.offline: print("FAIL give either --certificate (to verify against the published " "key) or --offline BUNDLE (to re-verify evidence you already hold).") return 2 if args.certificate and args.offline: print("FAIL --certificate and --offline are alternatives: the bundle " "already contains the certificate it was captured for. Passing both " "invites verifying one document while reporting on another.") return 2 # Validate the pin's SHAPE before anything else. A mistyped pin would # otherwise surface as a PCR0 mismatch, which reads as "the enclave is not # the one you expected" — sending someone to investigate a compromise when # what actually happened is a truncated copy-paste. if args.expect_pcr0 is not None: args.expect_pcr0 = args.expect_pcr0.strip().lower() if len(args.expect_pcr0) != 96 or any(c not in "0123456789abcdef" for c in args.expect_pcr0): print(f"FAIL --expect-pcr0 must be 96 hex characters (got " f"{len(args.expect_pcr0)}). PCR0 is SHA-384 over the enclave " f"image: 48 bytes, 96 hex characters.") return 2 try: from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature except ImportError: print("FAIL the 'cryptography' package is required: pip install cryptography") return 2 bundle = None if args.offline: try: with open(args.offline, "rb") as fh: bundle = json.load(fh) cert = bundle["certificate"] published = bundle["key"] except Exception as exc: print(f"FAIL could not read the bundle: {exc}") return 2 print(f"NOTE offline: no network requests made. Bundle captured " f"{bundle.get('captured_at', 'at an unrecorded time')} by verifier " f"{bundle.get('verifier_version', 'unknown')}.") else: try: with open(args.certificate, "rb") as fh: cert = json.load(fh) except Exception as exc: print(f"FAIL could not read the certificate: {exc}") return 2 checks: list[tuple[bool, str]] = [] version = cert.get("version") if version != CERT_VERSION_SUPPORTED: print(f"FAIL certificate version {version}; this verifier understands " f"{CERT_VERSION_SUPPORTED}. Refusing to guess at the field layout " f"of a document you are being asked to trust. Every published " f"version of this file stays downloadable — fetch the one that " f"matches, rather than trusting this one to improvise.") return 1 key_id = cert["key_id"] if bundle is None: published, source = _resolve_key(key_id, args.api, args.register) if published is None: print(f"FAIL could not retrieve key {key_id}. Tried: {source}") return 2 print(f"NOTE signing key resolved from the {source}.") checks.append((published.get("key_id") == key_id, f"published key matches the one the certificate cites ({key_id})")) pub_hex = published["public_key"] doc = bytes.fromhex(published["attestation_document"]) # The key must come from AWS's statement, not merely from the same JSON. try: payload = attestation_payload(doc) attested = payload.get("public_key") attested_hex = bytes(attested).hex() if attested else None except Exception as exc: print(f"FAIL attestation document is not readable: {exc}") return 1 checks.append((attested_hex == pub_hex, "the public key is the one inside the AWS attestation document")) # The chain. Everything above establishes what the document SAYS; this is # what makes it AWS's statement rather than merely a well-formed file. try: chain_checks, chain_info = verify_attestation_chain(doc) checks.extend(chain_checks) except Exception as exc: checks.append((False, f"AWS certificate chain is validatable ({exc})")) chain_info = {} pcrs = payload.get("pcrs") or {} pcr0 = bytes(pcrs[0]).hex() if 0 in pcrs else None # -- The measurement itself. ------------------------------------------- # # Until 2026-08-17 this file read PCR0 and then only PRINTED it, so nothing # here depended on it. That left one specific hole: a --debug-mode enclave # zeroes all three PCRs and attests to NOTHING, yet AWS still signs it a # real document with a valid chain. Every other check below passed and the # verdict was VERIFIED. publish-pcr0.py already refused an all-zero PCR0 for # exactly this reason; the knowledge existed on the publishing side and not # on the checking side, which is the wrong way round. checks.append((pcr0 is not None, "the attestation document carries a PCR0")) if pcr0 is not None: checks.append(( set(pcr0) != {"0"}, "PCR0 is not all zeros (all zeros means a --debug-mode enclave, " "which measures nothing and attests to nothing)", )) if args.expect_pcr0: checks.append((pcr0 == args.expect_pcr0, f"PCR0 matches the value you pinned " f"({args.expect_pcr0[:16]}...)")) else: print("NOTE --expect-pcr0 not given: PCR0 is reported below but is not " "compared against anything. A genuine attestation of an enclave " "you did not intend still verifies. To close that gap, obtain the " "measurement you mean to trust -- from a release note, or from us " "in writing -- and pass it here.") signed_body = bytes.fromhex(cert["signed_body"]) signature = bytes.fromhex(cert["signature"]) try: Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_hex)).verify(signature, signed_body) checks.append((True, "certificate signature verifies against the attested key")) except InvalidSignature: checks.append((False, "certificate signature verifies against the attested key")) # -- What the signature covers, and only that. --------------------------- # # The signature is over signed_body. client_digest, input_sha256, # output_sha256 and the rest of the JSON beside it are an unsigned # rendering of the same values. Up to 2026.08.18.2 the comparisons below # read THAT rendering, so a genuine signature could be paired with a # different result - new output_bytes and an output_sha256 to match - or # with a client_digest naming a request that was never sent, and every # check passed. Shown on a real certificate on 2026-09-22. Every # comparison now reads the signed copy, and a rendering that disagrees # with it fails here, by name. try: sb = SIGNED_BODY.unpack(signed_body) except struct.error: print(f"FAIL signed_body is {len(signed_body)} bytes; a version " f"{CERT_VERSION_SUPPORTED} certificate signs exactly {SIGNED_BODY.size}.") return 1 signed = {"version": sb[0], "key_id": sb[2].hex(), "client_digest": sb[3].hex(), "input_sha256": sb[4].hex(), "output_sha256": sb[5].hex(), "engine_digest": sb[6].hex(), "computed_at": sb[7]} differ = [f for f in signed if cert.get(f) != signed[f]] if cert.get("operation") not in (OPERATION_NAMES.get(sb[1]), f"0x{sb[1]:02x}"): differ.append("operation") checks.append((not differ, "the certificate's fields are the ones its signature covers" + (f" - these are NOT: {', '.join(differ)}" if differ else ""))) if args.request: want = signed["client_digest"] got = sha256_file(args.request) checks.append((want == got, f"the request you sent hashes to client_digest ({got[:16]}...)")) else: print("NOTE --request not given: cannot confirm this certificate is for YOUR request.") # The output side. `output_bytes` is what the signature actually covers; # the JSON you were shown is a rendering of it. Hashing these confirms the # rendering had something authentic behind it. ob = cert.get("output_bytes") if ob: import base64 raw = base64.b64decode(ob) got = hashlib.sha256(raw).hexdigest() checks.append((got == signed["output_sha256"], f"output_bytes hash to the signed output_sha256 ({got[:16]}...)")) if cert.get("output_encoding"): print(f"NOTE output_bytes decode as: {cert['output_encoding']}") elif args.result: want = signed["output_sha256"] got = sha256_file(args.result) checks.append((want == got, f"the result you hold hashes to output_sha256 ({got[:16]}...)")) else: print("NOTE this certificate carries no output_bytes, so the result it " "describes cannot be checked from here.") # The INPUT side -- gate 3 finding G3-2, and the reason this section exists. # # client_digest proves this certificate is about the request YOU sent. # input_sha256 proves what the engine actually computed over. Until # input_bytes existed, NOTHING CONNECTED THE TWO: the Gateway translates # your JSON into a struct and was simply trusted to do it faithfully. # # These bytes are that struct. Hashing them proves they are the ones the # enclave signed; decoding them with input_encoding lets you read whether # they say what you asked for. The hash check is mechanical and is done for # you below. THE READING IS YOURS -- no tool can know what you meant. ib = cert.get("input_bytes") if ib: import base64 raw_in = base64.b64decode(ib) got = hashlib.sha256(raw_in).hexdigest() checks.append((got == signed["input_sha256"], f"input_bytes hash to the signed input_sha256 ({got[:16]}...)")) decode_input_bytes(cert.get("operation", ""), raw_in, full=args.decode_input) eph = check_ephemeris_digest(raw_in, cert.get("operation", ""), args.ephemeris_sha256) if eph is not None: checks.append(eph) print("\n READ THE FIELDS ABOVE AGAINST WHAT YOU ASKED FOR. A matching " "hash proves this is the struct the enclave measured; it does NOT " "prove the struct says what you meant. That comparison is the one " "only you can make, and it is the whole point of this section.") if not args.decode_input: print(" Pass --decode-input to print every element rather than the " "first few.") else: print("NOTE this certificate carries no input_bytes. The certificate " "names your request via client_digest and states what the engine " "received via input_sha256, but NOTHING HERE CONNECTS THE TWO -- " "the translation from your JSON into the engine's struct happens " "outside the enclave and is not evidenced. Operations with very " "large requests do not echo them; see the documentation for how " "to check those.") print() for ok, label in checks: print(f" {'PASS' if ok else 'FAIL'} {label}") print() print(f" operation {cert.get('operation')}") print(f" engine digest {signed['engine_digest']}") print(f" enclave PCR0 {pcr0}") print(f" computed at {signed['computed_at']}") if chain_info.get("attested_at"): print(f" attested at {chain_info['attested_at']:%Y-%m-%d %H:%M:%SZ}") if args.print_chain: chain = chain_info.get("chain") or [] print(f"\n certificate chain ({len(chain)} certificates, root first):") for i, c in enumerate(chain): nb, na = _validity(c) role = "root" if i == 0 else ("leaf" if i == len(chain) - 1 else f"CA {i}") print(f" [{role}] {c.subject.rfc4514_string()}") print(f" valid {nb:%Y-%m-%d %H:%M:%SZ} .. {na:%Y-%m-%d %H:%M:%SZ}") print(f" sha256 {c.fingerprint(_sha256_alg()).hex()}") print() passed = all(ok for ok, _ in checks) # -- The bundle. -------------------------------------------------------- # # Written ONLY when every check passed. Exporting a bundle for a document # that failed would manufacture an artefact that looks like retained # evidence and is not — the single most dangerous file this tool could # produce, because its whole purpose is to be trusted years later by someone # who was not here today. if args.export_bundle: if not passed: print(" NOT EXPORTING: checks failed, and a bundle is meant to be " "evidence. Fix the failure, or keep the raw files instead.") else: out = { "bundle_version": 1, "captured_at": _utc_now(), "verifier_version": VERIFIER_VERSION, "verifier_sha256": _self_digest(), "cert_version": CERT_VERSION_SUPPORTED, "certificate": cert, # The whole point: the document carries its own AWS chain, so # this bundle needs nothing else and no one else. "key": { "key_id": published.get("key_id"), "public_key": published.get("public_key"), "engine_digest": published.get("engine_digest"), "pcr0": published.get("pcr0"), "attestation_document": published.get("attestation_document"), }, "verified_checks": [label for ok, label in checks if ok], "note": ( "Self-contained. Re-verify at any time with: " "geodesic_verify.py --offline . No network access " "and no Geodesic service is required. The attestation " "document carries the AWS certificate chain; the AWS Nitro " "root is pinned in the verifier and is independently " "obtainable from AWS." ), } try: with open(args.export_bundle, "w", encoding="utf-8") as fh: json.dump(out, fh, indent=2, sort_keys=True) print(f" BUNDLE WRITTEN: {args.export_bundle}") print( " Keep it, and keep a copy of this verifier beside it.") print( " From here the evidence no longer depends on Geodesic.") except Exception as exc: print(f" BUNDLE NOT WRITTEN: {exc}") return 2 if passed: print(" RESULT: VERIFIED") print(" The chain was checked as of the attestation timestamp, not now —") print(" AWS enclave leaf certificates live about three hours, while what") print(" they attest does not expire.") if not args.offline and not args.export_bundle: print() print(f" Signing keys are retained for at least " f"{GUARANTEED_RETENTION_DAYS} days from issuance, and in") print( " practice indefinitely. You do not have to rely on that:") print(f" capture this within {RECOMMENDED_CAPTURE_DAYS} days with " f"--export-bundle and the") print( " evidence becomes yours, verifiable offline with no request to us.") return 0 print(" RESULT: FAILED — do not rely on this document.") return 1 if __name__ == "__main__": sys.exit(main())