The job certificate format, version 1

Certificate format. A Hangar certificate is a signed file that shows what happened in a job, and anyone can check the file offline.

Use this page to build your own checker for Hangar certificates. The page gives every byte of the job certificate, the data certificate, the record, the tree head, the key attestation and the key set. The page also gives every byte of the application release documents that a job certificate includes (Section 12). A checker built from this page alone gives the same result as hangar verify on every test vector in vectors/cert/.

The hangar_cert crate is the reference implementation. If the crate and this page disagree, that is a defect. Report the defect to the address in SECURITY.md.

1. Conventions

2. The encoding profile

Every document is CBOR (RFC 8949) in the core deterministic encoding of RFC 8949 Section 4.2.1, with the narrower rules below. A decoder must refuse any input that breaks a rule. A decoder must never repair the input.

RuleDetail
IntegersMajor type 0 only, in the shortest form. Negative integers (major type 1) are refused
StringsByte strings (major type 2) and text strings (major type 3) with a definite length in the shortest form. Text is valid UTF-8
ArraysMajor type 4, definite count in the shortest form, at most 65,536 items
MapsMajor type 5, definite count, at most 65,536 entries. Every key is an unsigned integer. Keys are in strictly increasing numeric order, which for shortest-form integers is the bytewise order RFC 8949 requires. A repeated key is refused
Simple valuesfalse (0xf4) and true (0xf5) only. Floats, null, undefined and every other simple value are refused
TagsRefused. The document type is field 0 of each body instead
Indefinite lengthsRefused, as is the break code 0xff and additional information 28, 29 and 30
NestingAt most 16 levels, the top-level item being level 1
SizeAt most 8,388,608 bytes (8 MiB) for one document
Trailing bytesRefused. One document fills the input exactly

Unknown fields. Every map on this page lists all of its keys. A decoder refuses any key that the page does not list. This rule stops an attacker adding a field to a signed document that one verifier ignores while another verifier acts on the field.

Re-encoding. After decoding, a verifier encodes the document again with these rules and compares the new bytes with the input. Any difference is a failure. A decoder that follows the rules above never sees a difference. The check catches a decoder that accepts more than this page allows.

3. Digests and domain separation

The digest of a signed document is

D = SHA-384( ASCII(context) || 0x00 || encode(body) )

Here encode(body) is the canonical encoding of the body map of the document, not the signature bundle. The context is one of these strings:

DocumentContext stringBody field 0 (document type)
Recordhangar-record-v11
Tree headhangar-treehead-v12
Key attestationhangar-keyattest-v13
Certificatehangar-certificate-v14
Key sethangar-keyset-v15
Published tree head list (unsigned)none6
Application release manifest (Section 12.2)hangar-app-release-v17
Acceptance of a release (Section 12.3)hangar-app-acceptance-v18
Compiled envelope (Section 12.1)hangar-envelope-v19
Envelope request (Section 12.1)hangar-envelope-request-v110 (the request has no field 0; it sits inside a manifest)
Vendor release keyhangar-vendor-key-v111 (over the ML-DSA-87 key then the P-256 key, not CBOR)
Licence (Section 12.6)hangar-entitlement-v112
Release revocation list (Section 12.4)hangar-release-revocations-v113
Package manifesthangar-package-v114 (over the exact hangar_package.json bytes, not CBOR)
Site snapshot (Section 12.5, unsigned)hangar-site-snapshot-v115
Release bundle (Section 12.5, unsigned)hangar-release-bundle-v116
Configuration versionhangar-config-v117 (over the exact config.json bytes: compact JSON, keys sorted)
Policy versionhangar-policy-v118 (over CBOR {1: destinations sorted, 2: release limits map})
Narrowinghangar-narrowing-v119
Input valueshangar-input-values-v120 (over the exact values.json bytes)

No context contains a zero byte, so the hashed bytes can be read in only one way. The record digest is also the chain hash and the log leaf data. The certificate digest is the fingerprint of the certificate, printed on the certificate page. The release id is the digest of the exact bytes of a release manifest under hangar-app-release-v1. A document type number marks a domain even where the domain hashes JSON. No CBOR document can take such a number.

The log uses the hashes of RFC 9162 Section 2.1, with SHA-384:

leaf hash = SHA-384( 0x00 || leaf data )        leaf data = the 48-byte record digest
node hash = SHA-384( 0x01 || left || right )
empty tree root = SHA-384( "" )

Key id. A key pair has an ML-DSA-87 public key pk_ml (2,592 bytes) and a P-256 public key pk_ec (65 bytes). The key id is:

key id = first 16 bytes of SHA-384( pk_ml || pk_ec )

The file vectors/cert/digests.txt gives test values for each formula. The values were computed without the crate.

4. Signatures

Every signed document has the form

{1: body, 2: signature bundle}

Both signatures cover the 48-byte digest D of the body (Section 3).

AlgorithmWhat is signed and how
ML-DSA-87FIPS 204 ML-DSA.Sign, pure mode, empty context string, message D. Hedged: 32 random bytes per signature. Signature 4,627 bytes. Public key 2,592 bytes (pkEncode)
ECDSA P-256FIPS 186-5 ECDSA on P-256, message D, hash SHA-384: the value signed is the leftmost 256 bits of SHA-384(D). Deterministic nonce per RFC 6979 as the issuer implements it (a verifier does not depend on the nonce). Signature r || s, 32 bytes each, big-endian, 64 bytes, with s at most (n - 1) / 2 (low form). Public key SEC 1 uncompressed, 0x04 || x || y, 65 bytes

A WebCrypto or OpenSSL verifier checks the P-256 signature with message D, hash SHA-384, and the IEEE P1363 (r || s) format.

The signature bundle is the map

KeyFieldType
1key id of the signing key pairbytes, 16
2ML-DSA-87 signature, optional in the encodingbytes, 4,627
3ECDSA P-256 signature, optional in the encodingbytes, 64

The issuer always writes both signatures. A damaged document may lack key 2 or key 3, so the verifier can report which signature is missing. A bundle verifies only when both signatures are present and both are valid. A P-256 signature with a high s value fails.

5. The key hierarchy

KeyHoldsSignsValidity
RootML-DSA-87 and P-256 pair, published in the key setKey attestations and key sets onlyThe key set gives not_before and, after rotation, not_after
Day keyML-DSA-87 and P-256 pair, in memory on the control planeRecords, tree heads and certificatesThe attestation gives not_before and not_after, at most 93,600 seconds (26 hours) apart

A root signs a key attestation that vouches for each day key. A certificate includes the attestation for every day key that signed anything in the certificate. A verifier therefore needs only the key set.

A signed object is valid only if its time falls inside the window of the day key that signed the object. The times are the time of a record, the time of a tree head and the issued_at of a certificate. A day key that has expired does not invalidate objects that the key signed while the window was open.

5.1 Root key custody

The record-signing root of Hangar is separate from the release-signing root. Neither root is held in AWS (engine paper Section 1).

Together, the key set and this section state which of the two arrangements applies.

6. The record (document type 1)

A record is one state change. Signed form: {1: record body, 2: bundle}.

KeyFieldTypeRule
0document typeuint1
1format versionuint1
2chain idbytes, 16The organisation's chain, or the platform chain named in the key set
3indexuint0 for the first record of a chain, then one more than the record before
4previous record hashbytes, 48The record digest of the record before it. All zero for index 0
5time[seconds, nanoseconds]The control plane's clock
6clock error bounduintMilliseconds
7actormap {1: kind, 2: id}Kind: 1 customer, 2 operator, 3 system, 4 retention rule. Id: text
8record kinduintSee the table below
9subject idsarray of textAt most 32
10kind-specific bodymapSee Section 6.2

6.1 Record kinds

CodeKindCodeKind
1booking8credit
2purchase9flag
3movement10hold
4job state11release
5reset12advisory
6wipe13operator action
7attestation14key event
15release job16application release

Kind 15 records the events of one job of an application release: compiled, an attempt started or ended, an artefact checked, returned or cancelled. Kind 16 records a change to an application release: submitted, accepted, refused, revoked, a channel moved, a widening approved or declined. Kind 11 still means a release of Hangar itself.

6.2 The kind-specific body

The kind-specific body is a map with unsigned integer keys. Each type of value has a size limit, so a record cannot hold customer data:

The verifier reads the body without knowing the keys. The only exception is the movement record, below.

Movement records (kind 3).

KeyFieldType
1movement typeuint: 1 upload received, 2 staged for a slot, 3 mounted on the instance, 4 written by the job, 5 downloaded, 6 deleted, 7 never replicated elsewhere
2digests of the objects movedarray of bytes, 48 each
3total size in bytesuint, optional
4location (bucket and region, or instance and zone)text, optional
5slot idtext, optional
6the account that sent, fetched or askedtext, optional
7method (deletion method, or the request origin seen)text, optional
8the record digest of the wipe before a mountbytes, 48, optional
9the uploads an output derives fromarray of bytes, 48 each, optional

The control plane (D-02) defines the bodies of the other kinds and lists the bodies there. The test vectors use these bodies: job state {1: job id, 2: state (1 started, 2 finished), 3: image name, 4: image digest text, 5: resolution time, 6: command, 7: environment names, 8: exit code}; reset {1: instance id, 2: GPU UUIDs, 3: serials, 4: method}; wipe {1: instance id, 2: method, 3: result}; attestation {1: subject, 2: digest}; release {1: version, 2: manifest digest}.

A record from the slot egress proxy (D-03) has key 97. Key 97 is an array of text with one line for each decision of the proxy. Each line has the form <outcome> <destination> <bytes_up> <bytes_down> <at> <reason>. The outcome is refused or connected. The destination is the name that the job asked for, or - when the job gave no name. The byte counts are decimal. The time is RFC 3339. The reason is empty for a connection. Refusals go on a flag record, and connections go on a job state record. A record holds at most 256 lines, and each record links to the slot. The lines hold destinations and byte counts only, never content.

Application release and release job records (kinds 14, 15 and 16, and the movement and operator records of a job). This page fixes the keys from 100 up (hangar_cert::release::body). Keys 1 (action text), 2 (status text), 19 (reason text) and 39 (a 48-byte digest) are general keys of the control plane. The release check also reads these four keys.

KeyFieldType
100release idbytes, 48
101compiled envelope digestbytes, 48
102pool idtext, a UUID
103tenanttext, tnt_ and 32 hex digits
104configuration version digestbytes, 48
105attempt numberuint, from 1
106outcometext: succeeded, failed, cancelled, interrupted or outcome_unknown
107vendor key idbytes, 16
108vendor keys digest (hangar-vendor-key-v1)bytes, 48
109acceptance digestbytes, 48
110channeltext
111output nametext
112descriptor resulttext: passed or failed
113digest of the job's client referencebytes, 48
114release sequenceuint
115package nametext
116package versiontext
117job statetext
118widening itemsarray of text
119return digestbytes, 48
120licence serialuint
121key set digestbytes, 48
122entitlement digestbytes, 48
123deploymenttext, aws:<account>
124revocation list sequenceuint
125baseline release id: the release a job's widening is measured against (the release of the package the tenant last ran, or the package's first accepted release)bytes, 48
126advisory snapshot digest: the release vulnerability watch's mirror snapshot the release was matched against when the job started (D18.6)bytes, 48
127advisory snapshot timeuint, seconds
128advisory counts: six name=count words in the order critical high medium low unknown exploitedtext
129advisory decision: recorded, within_policy or releasedtext
130request key idtext, hgk_ and 24 hex digits
131request key capabilitiesarray of text
132digest of a tenant's referencebytes, 48
133expiryuint, seconds

A certificate of kind 4 (the return of one job) includes these records, listed by action text (key 1):

RecordKindBody
vendor release key registered14107, 108
release submitted16100, 114, 115, 116, 107
release accepted16 (platform chain, operator)100, 109
release refused, release revoked16100, 19
channel moved16115, 110, 100, 118
widening approval requested16100, 125, 110, 103, 118, and 39: the proposal digest
widening approved, widening declined16 (customer actor)100, 125, 110, 103, 118, and 39: the proposal digest
configuration set13 (customer actor)115, 102, 103, 104
compiled15100, 101, 102, 103 (with a tenant), 104, 113, 110, 125, 118 (only when the widening is not empty), 126 to 129 (all four or none: the advisory stamp), and 2: run or self_test
attempt started, attempt ended15 (subject the session)105, 106, 19
written (movement type 4)3one per output, the job id in key 5
artefact checked15111, 112, 39
returned15106, 117
cancelled15

The certificate includes the D-08 session records of each attempt (start, steps, reset steps with provider responses, connections and approvals) in the same form as on a sandbox session certificate.

7. The log

7.1 Inclusion proof

An inclusion proof is the map {1: leaf index (uint), 2: path (array of bytes, 48 each, at most 64)}. The path is the RFC 9162 PATH(m, D[n]), with the sibling nearest the leaf first. Verify the proof with RFC 9162 Section 2.1.3.2. Take the tree size and the root from the signed tree head, never from the proof.

7.2 Tree head (document type 2)

Signed form: {1: tree head body, 2: bundle}.

KeyFieldType
0document typeuint, 2
1versionuint, 1
2log idbytes, 16, equal to the key set's log id
3tree sizeuint
4root hashbytes, 48
5time[seconds, nanoseconds]
6clock error bounduint, milliseconds

7.3 Published tree head list (document type 6)

The list is an unsigned map. Each head in the list is signed.

KeyFieldType
0document typeuint, 6
1versionuint, 1
2headsarray of signed tree heads, sizes strictly increasing, one log id
3consistency proofsarray of arrays of bytes (48 each). Entry i links head i to head i + 1 (RFC 9162 Section 2.1.4)
4attestationsarray of signed key attestations, one for each day key that signed a head

8. Keys

8.1 Key set (document type 5)

The key set is the published trust anchor. Signed form: {1: key set body, 2: bundle}.

KeyFieldType
0document typeuint, 5
1versionuint, 1
2issuer nametext, "TALOS Defence Pty Ltd"
3issuer ABNtext, "24 696 691 906"
4not beforeuint, seconds
5rootsarray of root entries, 1 to 8
6platform chain idbytes, 16
7log idbytes, 16
8previous root key idbytes, 16, optional, present on rotation

Root entry: {1: key id (bytes, 16), 2: ML-DSA-87 public key (bytes, 2,592), 3: P-256 public key (bytes, 65), 4: not before (uint), 5: not after (uint, optional)}. The key id must equal the key id computed from keys 2 and 3.

A root listed in the key set signs the bundle. On rotation, key 8 names the previous root, and the previous root signs the bundle. The key set then lists both the old root (with not_after) and the new root. A verifier that holds the old key set accepts the new key set when all of these are true: the new key set checks on its own; the previous root of the new key set is a root of the old key set; the issuer, platform chain id and log id are unchanged; and the not_before of the new key set is not earlier than the old one.

8.2 Key attestation (document type 3)

Signed form: {1: attestation body, 2: bundle}. A root signs the bundle.

KeyFieldType
0document typeuint, 3
1versionuint, 1
2day key idbytes, 16, equal to the id computed from keys 3 and 4
3day key ML-DSA-87 public keybytes, 2,592
4day key P-256 public keybytes, 65
5not beforeuint, seconds
6not afteruint, seconds, greater than not before and at most 93,600 later
7root key idbytes, 16, equal to the bundle's key id

A verifier refuses an attestation if any of these is true: the key id of the bundle differs from key 7; the day key id equals the id of any root in the key set; key 7 is not a root in the key set; the window is longer than 26 hours; the validity of the root does not cover not before; or the bundle does not verify under the root.

9. The certificate (document type 4)

Signed form: {1: certificate body, 2: bundle}. A day key signs the bundle.

KeyFieldType
0document typeuint, 4
1versionuint, 1
2certificate idbytes, 16
3kinduint: 1 job certificate for a GPU slot, 2 job certificate for a sandbox session, 3 data certificate, 4 job certificate of an application release (the return of a job, API kind release_run). Any other number fails decode with the sentence "This certificate is of kind N, which this verifier does not know. Update the verifier and check it again."
4issuermap {1: name (text), 2: ABN (text)}
5organisationmap {1: legal name (text), 2: ABN or ACN (text), 3: chain id (bytes, 16)}
6factsmap, Section 9.1
7recordsarray of signed records, at most 4,096, in log order
8tree headsigned tree head
9inclusion proofsarray, one per record, same order
10attestationsarray of signed key attestations, at most 64
11issued at[seconds, nanoseconds]

9.1 Facts

KeyFieldType
1booked bytext
2jobsarray of job maps, at most 256
3hardwaremap, optional (absent on a data certificate)
4whenmap, optional (absent on a data certificate)
5data inarray of data items
6data outarray of data items
7movement recordsarray of record digests (bytes, 48)
8reset and wipe recordsarray of record digests (bytes, 48)
9instance attestation digestfact digest, optional
10release manifest digestfact digest, optional
11daily audit log digestfact digest, optional
12clock error bounduint, milliseconds
13operated bytext
14location statementtext
15subject upload digestbytes, 48, optional, present on a data certificate
16attestation kinduint, optional, present on a job certificate: 1 AWS Nitro attestation, 2 test attestation from the Hangar test pool, 3 AWS instance identity document only, for an instance type that has no hardware attestation (the Trainium2 pool). The verifier prints a notice on every report of a kind 3 certificate; 4 no attestation of the instance arrived (the worker never reached the control plane, or the job closed before it attested), and the verifier prints a notice on every report of a kind 4 certificate
17reset pathuint, optional, present on a job certificate: 1 fresh instance, 2 reboot in place (the same instance, reset and rebooted between jobs)
18sandbox factsmap, Section 9.4. Present on a sandbox session certificate (kind 2) and absent on every other kind
19release job factsmap, Section 9.5. Present on a job certificate of an application release (kind 4) and absent on every other kind
20platform versiontext, optional: the Hangar platform version in force when the certificate was issued, as major.minor.patch with no leading zeros, such as 4.0.1. A certificate of any kind states it once the control plane has a published platform version. A certificate issued before then omits key 20, and its bytes are what they were. Any other text at key 20 fails the facts check. The facts page prints a row "Hangar platform version"
22placesarray of place maps, at most 256, Section 9.6. Optional; required on a job certificate of the hosted service (kinds 1 and 2, and kind 4 from a hosted pool) whose key 20 is 4.17.0 or later
23Region policymap, Section 9.6. Optional; required wherever key 22 is required, and refused without key 22

A verifier that does not know key 20 fails decode with the unknown-key sentence (Section 2, unknown fields), so an older verifier never reads a certificate that states a platform version as if the certificate stated none. The certificate version stays 1: key 20 is an additive field, and the rule that a decoder refuses a key the page does not list is what makes the addition safe.

The issuer (certificate field 4) is the issuer that the record key set names (key set fields 2 and 3). Certificates from Hangar name TALOS Defence Pty Ltd as issuer. A job in a customer-controlled pool is signed under the customer's own root. The return from that job names the customer as issuer, and a verifier checks the return against the key set of the customer.

Job map: {1: job id (text), 2: image name (text, optional), 3: image digest (text), 4: resolved at (time), 5: command (array of text, at most 256), 6: environment names (array of text, at most 256), 7: started (time), 8: ended (time), 9: outcome (uint), 10: outcome reason (text, optional)}. Outcome: 1 completed (the command ran and exited), 2 ended abnormally (the job stopped before its command exited: a crash, an instance fault or the end of the slot), 3 cancelled (by the customer or an operator), 4 not started (booked and never run). The reason is a plain sentence from the worker, for example the exit status.

Hardware: {1: instance type, 2: instance id, 3: accelerator count (uint), 4: accelerators (array of {1: UUID or device identity, 2: serial}, at most 64), 5: region, 6: zone, 7: capacity block id (optional), 8: accelerator family (optional)}, text unless stated. Key 8 is nvidia-h100 or aws-trainium2. When key 8 is absent, the family is nvidia-h100, so certificates issued before the Trainium pool keep their meaning. A certificate from the pool hosted-trn-mel has aws-trainium2 at key 8, and each key 4 entry is {1: "neuron<N>@<bdf>", 2: the driver serial}. Any other text at key 8 fails decode. An entry that does not match the family also fails decode: a neuron<N>@<bdf> entry under any family but aws-trainium2, or any other entry under aws-trainium2. A verifier that does not know key 8 fails decode with the unknown-key sentence, so the verifier cannot misread the certificate. The facts page prints "with 1 Trainium2 accelerator" and one "Accelerator N" line for each device.

When: {1: zone name (text, "Australia/Melbourne"), 2: start (local time), 3: end (local time)}. Local time: {1: UTC time, 2: offset in minutes east of UTC (uint), 3: local time text}.

Data item: {1: digest (fact digest), 2: size in bytes (uint), 3: file name (text, optional)}.

9.2 Value rules on the certificate

ValueRule
TextAt most 1,024 bytes, no control characters
Fact digestBytes, exactly 32 (SHA-256, as AWS and OCI publish) or 48 (SHA-384)
Image digestText sha256: and 64 lower-case hexadecimal digits, or sha512: and 128
Environment nameText matching [A-Za-z_][A-Za-z0-9_]*, at most 128 characters. A name only: the grammar has no =

No certificate field holds free bytes. A certificate cannot show file contents, secret values or environment values.

9.3 Melbourne time

Victoria observes AEST (UTC+10, offset 600) and AEDT (UTC+11, offset 660). Daylight time starts at 02:00 AEST on the first Sunday in October and ends at 03:00 AEDT on the first Sunday in April; both moments are 16:00 UTC on the Saturday before that Sunday. For a UTC time t in UTC year Y, the offset is 660 if t is at or after the October change of Y or before the April change of Y, and 600 otherwise.

The local time text is YYYY-MM-DD HH:MM:SS AEDT or YYYY-MM-DD HH:MM:SS AEST. The text shows the UTC time plus the offset, zero-padded. Example: UTC 1793829600 (2026-11-04 22:00:00 UTC) is 2026-11-05 09:00:00 AEDT.

9.4 Sandbox facts (kind 2 only)

KeyFieldType
1instance typetext, a Graviton type such as m8g.xlarge
2instance idtext
3image release digestfact digest: the sandbox image release the session ran
4shapeuint: 1 c (2 GiB per vCPU), 2 m (4 GiB per vCPU), 3 r (8 GiB per vCPU)
5vCPUsuint: 2, 4, 8 or 16
6companionsarray, at most 16, of {1: name (text), 2: image digest (text, Section 9.2)}
7allowlist digestbytes, 48: SHA-384 of the canonical encoding (Section 2) of the destination allowlist as an array of text, in request order
8gateway connectionsuint: connections the gateway handled
9gateway connections refuseduint, at most key 8
10credential attachmentsarray of {1: secret name (text), 2: destination (text)}. The secret's value never appears
11approvalsarray of {1: proposal digest (bytes, 48), 2: decision (uint: 1 approved, 2 declined), 3: approver kind (uint: 1 a named person, 2 the account's Lock), 4: time}
12reset stepsarray, at most 32, of {1: step name (text), 2: provider response digest (bytes, 48), 3: time}. The response digest is SHA-384 of the provider's response text as the step record carries it
13tenanttext, optional: the tenant the session served, a vendor's downstream customer (D8.12). Absent for a session with no tenant

The published sandbox reset has five steps. These are the names that the worker uses: instance_terminated, egress_group_checked, credentials_revoked, gateway_ca_destroyed, secrets_detached. A sandbox session certificate lists each step exactly once and lists no other step. The step egress_group_checked replaces the earlier step security_group_deleted (D-20 N1). A verifier refuses a certificate that lists security_group_deleted.

The field names follow the D-08 control plane (hangar_api sandboxes): shape, vcpus, secret_name and destination, decisions approved and declined, and the reset step names. In D-08, the start record holds the allowlist as an array of text at body key 71, and each reset step record holds the response text of the provider at body key 76.

9.5 Release job facts (kind 4 only)

A job certificate of an application release is the return of one job (D15.19). Facts key 2 (jobs) may be empty, and key 3 (hardware) may be absent, because each attempt below lists its own instance. Keys 4 (when), 16 (attestation kind) and 17 (reset path) are present, as on every job certificate.

KeyFieldType
1job idtext
2tenanttext, optional: tnt_ and 32 hex digits
3releasesigned release, Section 12.2
4vendor key recordbytes, 48, optional (required from a hosted pool): the digest of the record that registered the vendor's release key
5widening approval recordbytes, 48, optional: the digest of the record that approved the release's widening for this tenant
6configuration digestbytes, 48 (hangar-config-v1)
7envelopethe compiled envelope, Section 12.1
8pool{1: id (text), 2: name (text), 3: kind (uint: 1 hosted, 2 customer-controlled), 4: provider, 5: site, 6: owner nationality, 7: jurisdiction, 8: assurance}, text unless stated
9attemptsarray, at most 4, of {1: number (uint), 2: instance type, 3: instance id, 4: zone, 5: purchase (spot or on_demand), 6: started (time), 7: ended (time), 8: outcome (uint), 9: reason (text, optional), 10: reset and wipe record digests (array of bytes, 48), 11: attestation (fact digest, optional)}
10inputsarray of {1: name, 2: digest (bytes, 48), 3: size (uint), 4: media type}
11values digestbytes, 48, optional (hangar-input-values-v1)
12outputsarray of {1: name, 2: digest (bytes, 48), 3: size (uint), 4: media type, 5: descriptor (uint: 1 passed, 2 failed), 6: attempt (uint)}
13connectionsarray of {1: destination (text), 2: connected (uint), 3: refused (uint)}
14model routes usedarray of {1: name, 2: destination, 3: fallback (bool)}
15approvalsarray, as sandbox facts key 11
16runtime versiontext
17outcomeuint: 1 succeeded, 2 failed, 3 cancelled, 4 interrupted, 5 outcome unknown
18self-testbool: true for the acceptance self-test of a release not yet accepted
19licenceuint, optional: 1 valid, 2 lapsed (a customer-controlled pool)
20revocation list ageuint, optional: whole days, when the job started (a customer-controlled pool)
21compiled recordbytes, 48: the digest of the compiled record
22release idbytes, 48
23envelope digestbytes, 48
24baseline releasesigned release, Section 12.2, optional: the release the widening is measured against, present when the compiled record's key 125 names another release than key 22; absent on a self-test

Keys 19 and 20 are both present on a return from a customer-controlled pool (pool kind 2) and both absent on a return from a hosted pool.

The return holds no input bytes, no output bytes, no configuration value and no secret value. The return holds only the digests, names and sizes of these items.

9.6 Places and the Region policy (facts keys 22 and 23)

Key 22 states each place where customer bytes were stored, processed or staged, and where the records about them are, by AWS Region. Key 23 states the Region policy the job ran under (D-20 WP19). Both are additive under certificate version 1, as key 20 is: a verifier that does not know them fails decode with the unknown-key sentence, so it never reads a certificate that states them as if they were absent. A data certificate states neither. A return from a customer-controlled pool states neither: a customer runtime keeps no Region policy.

Place: {1: role (uint), 2: region (text), 3: zone (text, optional), 4: what (text), 5: movement records (array of record digests, bytes 48, at most 4,096)}.

RoleNameWhat it is
1storedCustomer bytes at rest. What is the bucket name, control plane database for sealed sandbox logs, or the id of a sandbox session volume.
2processedCustomer bytes in use. What is the instance id.
3stagedCustomer bytes copied to the instance before the job. What is the instance id.
4recordDigests, sizes and names only. What is control plane, the cell that keeps the records, or control plane (home cell) for the organisation's home cell when another cell ran the job.

A movement record of type 2 (staged for a slot) or 3 (mounted on the instance) belongs to a staged place. A movement record of any other type belongs to a stored place.

Region policy: {1: setting (text), 2: source (uint), 3: policy record (bytes, 48), 4: cell (text), 5: home cell (text), 6: reason (text)}.

KeyFieldValues
1settingmelbourne_only, melbourne_preferred, no_preference, sydney_only or sydney_preferred. Any other text fails decode
2source1 the organisation setting, 2 the job's own setting. Any other number fails decode
3policy recordthe digest of the chain record that set the setting in force: for the organisation setting, its newest Region policy change, or the sign-up record when it never changed; for a job setting, the record "region policy set for a job", which states the setting and the source and is written with the job's submit record (for a visiting job, by the home cell before it sends the job). The record is on the home cell's chain, so a certificate of a visiting job names a record that the certificate does not include
4cellthe cell that the issuer names as the signer: sydney or melbourne. The certificate signature is checked against the key set given to the verifier (check 4); the facts check reads this name for rules 1 and 3
5home cellthe organisation's home cell: sydney or melbourne
6reasonpreferred_region, chip_not_in_preferred_region, no_capacity_in_start_window, first_to_start or chosen_block (a booked job on a block the person chose in the alternative Region, which the setting allows). Any other text fails decode

The location statement is derived. Key 14 equals the statement built from key 22 and key 23 by hangar_cert::places::location_statement, byte for byte. The issuer and the verifier call the same function. The Regions of processed and staged places are "processed"; the Regions of stored places are "stored"; each Region prints with its city, such as ap-southeast-4 (Melbourne):

For example: "Staged, processed and stored in ap-southeast-4 (Melbourne) only. Records in ap-southeast-4 (Melbourne). Region policy: melbourne only, set by the organisation. Keys held in Australia."

The facts page prints one row "Place" for each place, and the rows "Region policy", "Placement" (the sentence of the reason), "Cells" (signer named: sydney; home cell: melbourne) and "Policy record".

10. Verification procedure

Inputs: the certificate bytes, the key set bytes, the current time now in seconds, and optionally the published tree head list. The checks run in this order. The first failure stops the procedure. The report shows every later check as not run, never as passed.

  1. decode. The certificate decodes under Section 2 with only the fields of Section 9, every nested document included, and re-encodes to identical bytes.
  2. key_set. The key set decodes and re-encodes identically, lists distinct roots, names an issuer, and its bundle verifies under a root it lists (the previous root, on rotation).
  3. day_keys. The certificate carries at least one attestation, no day key is attested twice, and each attestation passes Section 8.2.
  4. certificate_signature. The bundle's key id is an attested day key, issued_at is inside its window, and the bundle verifies over the certificate digest.
  5. record_signatures. Every included record passes the same test, using the time of the record.
  6. chain. Every record's chain id is the organisation's chain id (field 5.3) or the key set's platform chain id. Within each chain, indices strictly increase in the order given. Where two included records are adjacent (index n and n + 1), the later one's previous record hash equals the earlier one's digest, and its time is not earlier by more than the larger of the two clock error bounds. Gaps are allowed: the log, not the chain, proves membership.
  7. inclusion. There is at least one record and exactly one proof per record. No two proofs name one leaf index and no record appears twice. Each record digest verifies at its leaf index against the tree head's size and root.
  8. tree_head. The tree head's log id equals the key set's log id, its bundle key id is an attested day key, its time is inside that key's window, and the bundle verifies.
  9. time. The key set's not_before is not after now. No record time, tree head time, issued_at or attestation not_before is later than now plus the clock error bound. No record is later than the tree head, and the tree head is not later than issued_at, each allowing the clock error bound.
  10. facts. The facts agree with the signed records. Check each of these:
    • The issuer equals the issuer of the key set.
    • Every data digest, the instance attestation digest, the release manifest digest, the daily audit log digest and the image digest of every job (as text) appears as a leaf value in the body of some included record.
    • Each listed movement record digest is the digest of an included movement record. Each reset and wipe record digest is the digest of an included reset or wipe record.
    • The accelerator count equals the number of accelerators listed. Each entry has the form of the accelerator family (Section 9.1).
    • Each local time equals the time that Section 9.3 gives for the UTC time. The zone is Australia/Melbourne.
    • A job ends no earlier than it starts. A job starts no earlier than its image name resolved.
    • A job certificate states hardware, when, the attestation kind and the reset path.
    • A sandbox session certificate has sandbox facts. Every other kind has none.
    • For sandbox facts, all of these hold. The vCPUs are 2, 4, 8 or 16. Refused connections do not exceed connections. The instance type and id equal those in hardware. The allowlist digest is the SHA-384 of the canonical encoding of an array that an included record holds. An included record holds the proposal digest of each approval, as bytes. Each published reset step appears exactly once, and no other step appears. The response digest of each step is the SHA-384 of a text value that an included record holds. A tenant, when present, is a text value that an included record holds.
    • A data certificate names its subject upload. Every included record is a movement record whose key 2 or key 9 contains the digest of that upload. The records are in time order. The first record shows the upload being received.
    • Places and the Region policy (Section 9.6). A job certificate of the hosted service whose platform version is 4.17.0 or later states key 22 and key 23. Key 23 is not stated without key 22. When key 22 is stated, all of these hold. Each listed movement record is named by exactly one place, of the role its type gives. A place names no movement record that the facts do not list. The instance of the hardware, and of each attempt of a release job, is a processed place, in the Region the hardware states. The location statement is the derived one.
    • When key 23 is stated, all of these hold, in this order. The cell and the home cell are cells of the hosted service. For melbourne_only and sydney_only, every place of every role is in that Region, the cell is the cell of that Region, and the home cell is the cell of that Region. Every place of every role is in the Region of a hosted cell that the setting allows: melbourne for melbourne_only, sydney for sydney_only, and sydney and melbourne for the three other settings. For melbourne_preferred and sydney_preferred, a processed place outside the preferred Region has the reason chip_not_in_preferred_region, no_capacity_in_start_window or chosen_block. Every processed place is in the Region of the cell that key 23 names.
  11. release (kind 4 only, listed straight after facts; Section 10.1). The vendor signed the release, TALOS Defence accepted the release, the compiled envelope lies within the request of the release, and every connection and output stays within the envelope.
  12. published_heads (only when the list is supplied). The list's heads have one log id and strictly increasing sizes; each consistency proof verifies; each head is signed by a day key the list's attestations vouch for, within its window; and the certificate's tree head (log id, size and root) is one of the heads. If the list does not contain the tree head of the certificate, the certificate comes from a log that was never published. The test therefore detects a fork of the log.

For kind 4, check 10 also requires: release job facts present (and absent on every other kind); when, the attestation kind and the reset path stated.

The certificate is valid when every check but published_heads passes and published_heads did not fail. For kinds 1 to 3, the checks are 1 to 10, and the reports list eleven checks exactly as before. For kind 4, the checks also include release. The report says whether published_heads ran.

10.1 The release check

Inputs: the certificate, the record key set, and TALOS Defence's published release key set (hangar verify --release-keys). The parts run in order. The first part that fails stops the check and gives the failure sentence.

  1. The release id (Section 12.2) equals release job facts key 22. The record whose digest is at key 21 is included and is a release job record (kind 15) with action compiled. The body of that record has the release id at key 100 and the pool id at key 102. The body has the tenant at key 103 (absent when the job has no tenant). The body has self_test at key 2 for a self-test, or run otherwise.
  2. The vendor signature bundle names the vendor keys in the release (key id as in Section 3). Both signatures verify over the release id. A return from a hosted pool carries facts key 4. A customer-controlled runtime cannot include a record of the Hangar chain, so there the acceptance binds the key. When facts key 4 is present, the record that key 4 names is included. That record is a key event (kind 14) on the vendor chain (the vendor chain of the manifest) with action vendor release key registered. The record has the vendor key id at key 107 and the vendor keys digest at key 108. A self-test runs on a hosted pool with no tenant. A return with facts key 18 true fails if the pool is customer-controlled, if the return names a tenant, if the return carries an acceptance, or if the return carries facts key 5.
  3. Unless the job is a self-test, all of these are true:
    • The release carries an acceptance.
    • The verifier has the release key set. Otherwise the check fails with "This return needs TALOS Defence's published release key set to check the release. Give it with hangar verify --release-keys."
    • The acceptance names this release, this vendor key and this vendor keys digest.
    • Every published acceptance check appears exactly once and passed. The exception is advisory_match, which may be not run.
    • The key id of the acceptance bundle is a root of the release key set that is valid at the acceptance time. A day key is never a root.
    • That root is not a root of the record key set, because release roots and record roots are separate keys.
    • Then the widening rules apply. The compiled record has a baseline release id at key 125. When the baseline release id equals the release id, facts key 24 is absent and the widening is empty. Otherwise facts key 24 is present, and all of these are true: the release id in key 24 equals key 125; key 24 names the same package and vendor chain as the release; the vendor signature of key 24 verifies; and the acceptance of key 24 verifies against the release key set, by a root that is not a root of the record key set. The widening is the difference diff (Section 12.1) from the envelope request of the baseline to the envelope request of the release. Each item is printed as describe prints it. Key 118 of the compiled record (absent when empty) lists exactly those items, in that order. A widening that is not empty needs facts key 5. The widening list inside the acceptance is information only and decides nothing. A self-test carries no facts key 24. When facts key 5 is present, the record that key 5 names is included. That record is an application release record (kind 16) with action widening approved. The record has the release id at key 100 and the tenant of the job at key 103. For a job with no tenant, key 103 is absent or the empty text.
  4. The envelope digest (hangar-envelope-v1 over the canonical bytes of the envelope) equals facts key 23 and key 101 of the compiled record. The envelope lies within the request of the release (Section 12.1, the rule within). The first source of the envelope is the envelope request digest of the release. The envelope states the release id, the id, name and kind of the pool, the tenant, the configuration digest, the inputs and the values digest, and each agrees with the facts. The configuration digest also equals facts key 6 and key 104 of the compiled record. Every input is in data in, with its size. A return from a customer-controlled pool states facts keys 19 and 20. A return from a hosted pool states neither.
  5. Every destination with a connection made is in the destinations of the envelope. A destination with only refusals may have any name. Every model route used is a route of the envelope, to the destination of that route, or to the declared fallback when the entry is marked as a fallback. An included record holds the proposal digest of every approval, as bytes.
  6. There are at most retries plus one attempts, numbered from 1 in order. Each attempt starts no earlier than the not-before time of the envelope and no later than the not-after time, allowing for the clock error bound. Each attempt ends no earlier than the attempt starts. The reset and wipe records of each attempt are included reset or wipe records. Every output names a listed attempt and is in data out with its size. Every output also has an included release job record with action artefact checked, the output name at key 111, the descriptor result at key 112 and the output digest at key 39. A job that states success has a last attempt that succeeded, and every output of that attempt passed.

The compiled record may hold the advisory stamp of the release vulnerability watch (Section 6.2, keys 126 to 129). Part 1 reads the stamp for every job. The record has all four keys or none. The digest is 48 bytes, the time is a uint, the counts are text in the stated form, and the decision is one of the three words. A record with no stamp is valid, so a return from before the watch verifies as before. The stamp shows what the control plane knew when the job started. The stamp does not show that the release has no vulnerabilities.

The browser verifier has no release key set. For a return that is not a self-test, the browser verifier therefore fails part 3 with the sentence above. The browser verifier never passes a release that it could not check.

Notices. Every report, valid or not, prints these facts as notices beside the verdict. The JSON field notices holds the notices, and the terminal shows them as NOTICE: lines. Notices are not checks, so a report never shows a notice as passed:

The facts page of a kind 4 return adds these rows after the sandbox rows: Release (<package> <version>, sha384:<12 hex>), Tenant, Pool (<name>, <site>), Envelope (sha384:<12 hex>, <n> destinations, <v> vCPU, <m> min), Attempts, Outcome, and Self-test (yes, for a self-test only).

11. A worked start

The test vector for a slot certificate begins like this:

a2                         map, 2 entries (signed form)
  01                       key 1: body
  ac                       map, 12 entries
    00 04                  document type 4
    01 01                  version 1
    02 50 <16 bytes>       certificate id
    03 01                  kind 1, GPU slot
    04 a2                  issuer, map of 2
       01 75 "TALOS Defence Pty Ltd"     21-byte text
       02 6e "24 696 691 906"           14-byte text
    05 a3 ...              organisation
    06 ...                 facts
    ...
  02 a3                    key 2: bundle, map of 3
    01 50 <key id>
    02 59 12 13 <4,627 bytes>
    03 58 40 <64 bytes>

Every record inside the certificate begins a2 01 ab 00 01 01 01 02 50 <chain id> 03 <index>. The bytes show a signed form, then a record body of 11 entries, with type 1 and version 1.

12. The application release documents

These documents describe an application release and let a customer-controlled pool check the release offline. The release check (Section 10.1) reads them. Every document follows the encoding rules of Section 2 and lists all of its keys. A verifier refuses a document when a list that the page calls "sorted" is out of order or repeats an item. The version is 1 in every document.

12.1 The envelope request and the compiled envelope (types 10 and 9)

The envelope request sits inside the release manifest. The request has no field 0. The digest of the request is hangar-envelope-request-v1 over the canonical bytes of the request.

KeyFieldType
1destinationsarray, at most 64, sorted by name, of {1: name, 2: purpose (text), 3: required (bool)}
2data scopesarray, sorted, of uint: 1 the job's inputs, 2 none
3secretsarray, sorted by name, of {1: name, 2: destination, 3: header, 4: scheme (optional), 5: required (bool)}
4companionsarray, at most 8, sorted by name, of {1: name, 2: image digest (text)}
5model routesarray, sorted by name, of {1: name, 2: destination, 3: fallback (optional)}
6person-present stepsarray, sorted by action then target, of {1: action (uint: 1 destination first use, 2 credential use, 3 agent action), 2: target, 3: sentence}
7compute{1: vCPUs, 2: memory MiB, 3: minutes per attempt}, each at least 1
8failure policy{1: retries (0 to 3), 2: resume from a checkpoint (bool)}

Names: a destination is a lower-case DNS name of two or more labels. A destination is not an address, does not end in .hangar.internal and has no trailing dot. A secret is a capital letter then up to 63 capitals, digits or underscores. A header is lower-case letters, digits and hyphens, and is not a header that the gateway refuses. A companion is a lower-case letter then up to 30 lower-case letters, digits or hyphens, and is never agent. A secret is bound to a requested destination. The destination and the fallback of a model route are requested destinations, and the two differ. A destination first use step names a requested destination. A credential use step names a requested secret.

The compiled envelope (type 9). The envelope digest is hangar-envelope-v1 over the canonical bytes of the compiled envelope.

KeyFieldType
0document typeuint, 9
1versionuint, 1
2release idbytes, 48
3pool{1: id (text), 2: name (text), 3: kind (uint: 1 hosted, 2 customer-controlled)}
4tenanttext, optional
5image{1: digest (text), 2: platform (uint: 1 linux/arm64, 2 linux/amd64)}
6commandarray of text, 1 to 256
7companionsas request key 4
8destinationsarray of text, sorted: the gateway's allow list, and no other
9droppedarray, sorted by item, of {1: item (a requested destination, secret or route name), 2: reason}
10 to 13data scopes, secrets, model routes, person-present stepsas request keys 2, 3, 5 and 6
14, 15compute, failure policyas request keys 7 and 8
16, 17not before, not after[seconds, nanoseconds]
18configuration digestbytes, 48
19sources{1: request digest, 2: account policy digest, 3: tenant policy digest (optional), 4: narrowing digest}, bytes, 48 each
20inputsarray, sorted by name, of {1: name, 2: digest (bytes, 48), 3: size, 4: media type}
21values digestbytes, 48, optional

A compiled envelope has these properties without reference to a request. Every secret, model route and fallback is bound to a granted destination. The envelope grants the data scope of the inputs of the job whenever the envelope binds an input. The window is positive and at most (minutes + 10) * (retries + 1) * 60 seconds.

Within. A compiled envelope lies within the request of a release when all of these are true:

Widening. Between two requests, the widenings are the following, in this order and by name within each: a new destination; a new or rebound secret; a new data scope; a new companion or one on another image; a new model route or one to another destination; a fallback gained or changed; more vCPUs, memory or minutes; more retries or resume newly allowed; a person-present step removed. A request with no widening needs no approval.

12.2 The release manifest and the signed release (type 7)

KeyFieldType
0document typeuint, 7
1versionuint, 1
2vendor{1: chain id (bytes, 16), 2: legal name (text)}
3packagetext: a lower-case letter then 1 to 62 lower-case letters, digits or underscores
4versiontext, MAJOR.MINOR.PATCH, digits only, no leading zeros
5package manifest digestbytes, 48 (hangar-package-v1)
6imageas envelope key 5
7commandarray of text, 1 to 256
8resources{1: vCPUs, 2: memory MiB, 3: disk GiB, 4: accelerator (uint: 0 none, 1 H100, 2 Trainium2), 5: minutes per attempt, 6: further accelerators the release also runs on (array of uint, optional, one or two codes in ascending order, none equal to key 4 and none repeated)}
9companionsequal to the request's companions
10SBOM digestbytes, 48
11envelope requestSection 12.1
12envelope request digestbytes, 48, equal to the request's digest
13configuration schema digestbytes, 48
14runtime range{1: minimum, 2: below}, each MAJOR.MINOR.PATCH, minimum below below
15determinismuint: 1 deterministic, 2 within the envelope
16sequenceuint, from 1
17created at[seconds, nanoseconds]

The compute of the request lies inside the resources. The release id is hangar-app-release-v1 over the exact canonical bytes of the manifest. No field names an image by tag, and no field holds a configuration value.

The signed release: {1: manifest bytes, 2: vendor signature bundle, 3: {1: ML-DSA-87 public key, 2: P-256 public key}, 4: acceptance (optional), 5: acceptance signature bundle (optional)}. Key 1 holds the exact bytes of the manifest, decoded strictly. Keys 4 and 5 are both present or both absent. The vendor bundle signs the release id (Section 4). The vendor keys digest is hangar-vendor-key-v1 over the ML-DSA-87 key followed by the P-256 key.

12.3 The acceptance (type 8)

KeyFieldType
0document typeuint, 8
1versionuint, 1
2release idbytes, 48
3vendor key idbytes, 16
4vendor keys digestbytes, 48
5checksarray of {1: name, 2: result (uint: 1 passed, 2 failed, 3 not run), 3: detail digest (optional)}
6wideningarray of text
7self-test returnsarray of {1: pool name, 2: certificate digest (bytes, 48)}
8accepted at[seconds, nanoseconds]

The published checks, each listed once, are: vendor_signature, image_present, sbom_present, advisory_match, envelope_request, configuration_schema, runtime_range, envelope_difference, self_test. A root of the release key set signs the acceptance digest, which is hangar-app-acceptance-v1 over the canonical bytes of the acceptance.

12.4 The release revocation list (type 13)

The list is the map {0: 13, 1: 1, 2: sequence (uint, from 1), 3: issued at, 4: revoked}, where revoked is an array sorted by release id of {1: release id (bytes, 48), 2: at, 3: reason (text)}. Each list holds every revocation made so far. Signed form {1: list, 2: bundle}. A root of the release key set that is valid at the issue time signs the bundle, over hangar-release-revocations-v1 of the list.

12.5 The release bundle and the site snapshot (types 16 and 15, unsigned)

A root signs each item inside, so neither document is signed as a whole. A key set inside is stored as the exact published bytes of the key set.

Neither document holds image layers. The vendor ships a separate OCI layout, and the importer checks the layout by digest against the manifest.

12.6 The licence (type 12)

KeyFieldType
0document typeuint, 12
1versionuint, 1
2serialuint, from 1
3customer{1: legal name, 2: ABN, 3: chain id (bytes, 16)}
4pool idtext
5deploymentsarray, 1 to 16, of {1: provider (uint: 1 AWS, 2 Azure), 2: id (an AWS account of 12 digits, or a lower-case Azure subscription UUID)}
6capability{1: profiles (array of text), 2: packages (uint), 3: tenants (uint)}
7supportuint: 1 standard, 2 extended
8update channeltext
9not beforeuint, seconds
10not afteruint, seconds, after not before
11issued at[seconds, nanoseconds]

The licence file is {1: licence, 2: bundle}. A root of the entitlement key set that is valid at the issue time signs the bundle, over hangar-entitlement-v1 of the licence. The offline check reads the file, the signature, the deployment and the term, in that order. The check refuses a file that does not decode, a bad signature, another deployment, or a term that has not started. Inside the term, the licence is valid. After the term, the licence is lapsed. A lapsed licence still runs installed releases and refuses new release imports. A lapsed licence admits new tenants for 90 days. The check reports nothing to TALOS Defence.

Read this page as Markdown