A build ledger is a document providing a completeness guarantee for inputs and outputs during a build process as strictly-ordered entries with a chaining signature. It is produced by the BuildWarden relay — an ephemeral TLS-terminating proxy that observes all network traffic during an isolated build.
The ledger is designed to be:
<ledger_root>/
├── ledger # The binary ledger file
├── ledger.cert.pem # Public certificate in PEM format (convenience copy)
├── payloads/
│ └── <hex_hash> # Payload files stored by primary hash
├── artifacts/
│ └── <name> # Artifact files (or symlinks to payloads/)
└── metadata/
└── <stream> # Optional append-only metadata streams
The ledger file is a concatenation of:
[Header] [Record 0] [Record 1] ... [Record N]
There are no delimiters between records. Record boundaries are determined by the record structure and sizes declared in the header.
The header establishes the cryptographic parameters, embeds the public key, and provides informational metadata.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0 | 4 | Magic | ASCII BLDL (0x42 0x4C 0x44 0x4C) |
| 4 | 1 | Version | Ledger spec version (0x01 for this spec) |
| 5 | variable | Signature scheme | Null-terminated UTF-8 string (e.g., ed25519-sha512\0) |
| — | 2 | Signature size | uint16 — byte length of all signatures in this ledger |
| — | 2 | Hash block size | uint16 — total byte length of concatenated hashes per payload |
| — | 2 | Public key length | uint16 — byte length of the public key |
| — | variable | Public key bytes | Raw public key (length per above field) |
The binary prefix ends immediately after the public key bytes. Its total size is:
5 + len(signature_scheme) + 1 + 2 + 2 + 2 + public_key_length
Immediately following the binary prefix:
| Size | Field | Description |
|---|---|---|
| signature-size | Header signature | Signs all bytes of the binary prefix |
Signature input: All bytes from offset 0 through the end of the public key bytes (the entire binary prefix).
The header signature serves as the chain root — it becomes the prev_sig for the first record.
Following the header signature:
| Size | Field | Description |
|---|---|---|
| 4 | Metadata length | uint32 — byte length of the CBOR object that follows |
| variable | CBOR object | Informational header metadata |
The header metadata is a CBOR map containing:
{
"hashes": ["blake2b_256", "sha256", "sha1", "md5"],
"schemas": [
"https://github.com/buildwarden/buildwarden/schemas/http-open.json",
"https://github.com/buildwarden/buildwarden/schemas/http-headers.json",
"https://github.com/buildwarden/buildwarden/schemas/http-body.json",
"https://github.com/buildwarden/buildwarden/schemas/artifact.json",
"https://github.com/buildwarden/buildwarden/schemas/redacted.json"
],
"environment": {
"type": "container",
"digest": "<container_digest>"
}
}
| Key | Type | Description |
|---|---|---|
hashes |
array of text | Ordered list of hash algorithm names. The concatenation of their outputs equals hash_block_size bytes. |
schemas |
array of text | Ordered list of JSON Schema URLs. Index position maps to the schema index byte in records. |
environment |
map | Informational build environment description. |
The header metadata is not signed and may be modified without invalidating the ledger. It is informational context for consumers.
Note: The schemas list may be updated after ledger creation (e.g., appending new schemas) without invalidating the signature chain, since header metadata is not signed. Existing schema indices remain stable.
| Byte Value | Name | Description |
|---|---|---|
| 0x01 | open |
Opens a new channel |
| 0x02 | checkpoint |
Intermediate data on an open channel |
| 0x03 | close |
Closes a channel |
| 0x04 | artifact |
Closes a channel, marking the payload as a build artifact |
[Record type: 1 byte] — 0x01
[Previous signature] — signature-size bytes
[Payload size: 8 bytes] — int64, big-endian (typically 0)
[Hash block] — hash-block-size bytes (OMITTED if payload size = 0)
[Record signature] — signature-size bytes
[Schema index: 1 byte] — 0-254 = index, 255 = no metadata
[Metadata length: 4 bytes] — uint32 (OMITTED if schema index = 255)
[CBOR metadata] — (OMITTED if schema index = 255)
Open records do not include an open-signature field.
Signature input:
record_type(0x01) + prev_sig + payload_size
When payload size = 0 (the common case), the signature input is:
0x01 + prev_sig + 0x0000000000000000
When payload size ≠ 0, the hash block is appended to the signature input:
0x01 + prev_sig + payload_size_bytes + hash_block
[Record type: 1 byte] — 0x02
[Previous signature] — signature-size bytes
[Open signature] — signature-size bytes
[Payload size: 8 bytes] — int64, big-endian (sign = direction)
[Hash block] — hash-block-size bytes (OMITTED if payload size = 0)
[Record signature] — signature-size bytes
[Schema index: 1 byte] — 0-254 = index, 255 = no metadata
[Metadata length: 4 bytes] — uint32 (OMITTED if schema index = 255)
[CBOR metadata] — (OMITTED if schema index = 255)
Signature input:
record_type(0x02) + prev_sig + open_sig + payload_size_bytes + hash_block
When payload size = 0:
0x02 + prev_sig + open_sig + 0x0000000000000000
[Record type: 1 byte] — 0x03
[Previous signature] — signature-size bytes
[Open signature] — signature-size bytes
[Payload size: 8 bytes] — int64, big-endian (sign = direction)
[Hash block] — hash-block-size bytes (OMITTED if payload size = 0)
[Record signature] — signature-size bytes
[Schema index: 1 byte] — 0-254 = index, 255 = no metadata
[Metadata length: 4 bytes] — uint32 (OMITTED if schema index = 255)
[CBOR metadata] — (OMITTED if schema index = 255)
Signature input: Identical structure to checkpoint.
record_type(0x03) + prev_sig + open_sig + payload_size_bytes + hash_block
[Record type: 1 byte] — 0x04
[Previous signature] — signature-size bytes
[Open signature] — signature-size bytes
[Payload size: 8 bytes] — int64, big-endian
[Hash block] — hash-block-size bytes (OMITTED if payload size = 0)
[Record signature] — signature-size bytes
[Schema index: 1 byte] — 0-254 = index, 255 = no metadata
[Metadata length: 4 bytes] — uint32 (OMITTED if schema index = 255)
[CBOR metadata] — (OMITTED if schema index = 255)
Signature input: Identical structure to close.
record_type(0x04) + prev_sig + open_sig + payload_size_bytes + hash_block
The artifact record closes its channel (same semantics as close). Its record type structurally identifies the payload as a build output.
Constraints:
out direction), though validators MUST NOT reject based on sign alone — the record type is authoritative.The payload size field is a signed 64-bit big-endian integer.
| Value | Meaning |
|---|---|
| > 0 (positive) | Data flowing into the build (response bodies, downloaded resources) |
| < 0 (negative) | Data flowing out of the build (request bodies, artifact submissions) |
| = 0 | No payload / directionless |
The absolute value of the payload size is the byte length of the payload content. When non-zero, the hash block immediately follows the payload size field.
When payload size = 0, the hash block is omitted entirely — no bytes are present for it. This applies uniformly to all record types.
The hash block is the raw concatenation of hash digests in the order declared by the header’s hashes array. Its total size equals the header’s hash_block_size field.
Example with default hashes ["blake2b_256", "sha256", "sha1", "md5"]:
| Hash | Output Size |
|---|---|
| blake2b_256 | 32 bytes |
| sha256 | 32 bytes |
| sha1 | 20 bytes |
| md5 | 16 bytes |
| Total | 100 bytes |
The hash block size declared in the header would be 100 (0x0064).
Payload content is written to payloads/<primary_hash_hex> where the primary hash is the first algorithm in the header’s hash list.
The signature scheme is declared as a null-terminated string in the header. The scheme name encodes both the signing algorithm and the digest algorithm.
| Scheme | Signing Algorithm | Digest | Key Size | Signature Size |
|---|---|---|---|---|
ed25519-sha512 |
Ed25519 | SHA-512 | 32 bytes | 64 bytes |
rsa-pkcs1v15-sha512 |
RSA PKCS#1 v1.5 | SHA-512 | variable | variable |
For all schemes:
Each record’s signature incorporates the previous record’s signature (prev_sig), forming a hash chain:
Header sig ← Record 0 sig ← Record 1 sig ← ... ← Record N sig
Altering, removing, or reordering any record breaks the chain for all subsequent records.
Each record carries a 1-byte schema index:
| Value | Meaning |
|---|---|
| 0–254 | Index into the header’s schemas array |
| 255 | No metadata attached |
When schema index = 255, no metadata length or CBOR bytes follow. When schema index is 0–254, a 4-byte uint32 length followed by that many bytes of CBOR-encoded metadata immediately follow.
Metadata is never part of the signature input. It can be freely added, modified, or stripped without affecting ledger validity.
Each record has at most one metadata attachment. The schema index identifies which schema the CBOR object conforms to.
BuildWarden defines the following default schemas. Their indices are determined by position in the header’s schemas array (not fixed by this spec).
http-openAttached to open records for HTTP/HTTPS channels.
{
"method": "GET",
"url": "https://example.com/path",
"protocol": "HTTP/1.1"
}
| Field | Type | Description |
|---|---|---|
method |
text | HTTP method |
url |
text | Full request URL |
protocol |
text | Protocol version |
http-headersAttached to checkpoint records carrying HTTP headers as payload.
{
"headers": [
["X-Amz-Request-Id", "abc123"],
["X-Custom-Header", "value"],
["Authorization", "<redacted>"],
["Cookie", "<redacted>"],
["Set-Cookie", "<redacted>"]
]
}
| Field | Type | Description |
|---|---|---|
headers |
array of [name, value] | Non-standard headers. Auth-related headers (Authorization, Cookie, Set-Cookie, Proxy-Authorization) are listed with value <redacted>. Standard/structural HTTP headers (those defined in HTTP/1.1 and HTTP/2 base specifications) are omitted — they exist in the raw payload bytes. |
http-bodyAttached to checkpoint or close records carrying HTTP body content as payload.
{
"status": 200
}
| Field | Type | Description |
|---|---|---|
status |
unsigned int | HTTP response status code (present only for response bodies) |
artifactAttached to artifact records.
{
"name": "my-binary",
"context": {
"content_type": "application/octet-stream"
}
}
| Field | Type | Description |
|---|---|---|
name |
text | Filename as stored in the ledger’s artifacts/ directory |
context |
map | Freeform key-value map for additional artifact metadata |
redactedAttached to any record whose original metadata has been stripped or was never recorded.
{
"owner": "corp.amazon.com"
}
| Field | Type | Description |
|---|---|---|
owner |
text | Identifier of the party responsible for or owning the redacted information |
For HTTP/HTTPS requests proxied by the relay, the typical entry sequence is:
| # | Type | Payload Dir | Payload Content | Schema |
|---|---|---|---|---|
| 1 | open | — (0) | — | http-open |
| 2 | checkpoint | out (−) | Request headers (raw bytes) | http-headers |
| 3 | checkpoint | in (+) | Response headers (raw bytes) | http-headers |
| 4 | close | in (+) | Response body | http-body |
| # | Type | Payload Dir | Payload Content | Schema |
|---|---|---|---|---|
| 1 | open | — (0) | — | http-open |
| 2 | checkpoint | out (−) | Request headers (raw bytes) | http-headers |
| 3 | checkpoint | out (−) | Request body | http-body |
| 4 | checkpoint | in (+) | Response headers (raw bytes) | http-headers |
| 5 | close | in (+) | Response body | http-body |
| # | Type | Payload Dir | Payload Content | Schema |
|---|---|---|---|---|
| 1 | open | — (0) | — | http-open |
| 2 | checkpoint | out (−) | Request headers (raw bytes) | http-headers |
| 3 | artifact | out (−) | Artifact body | artifact |
Metadata is excluded from all signatures. This allows:
http-open metadata.redacted schema (changing the schema index byte and CBOR content).After redaction, the signature chain remains fully valid. The content identity (payload size + hash block) is preserved, proving what was transferred without revealing where it came from.
The redacted schema provides attribution — identifying who owns or is responsible for the redacted information — enabling downstream consumers to request the full metadata from the appropriate party if needed.
A channel is complete when its open entry has a corresponding close or artifact entry.
A ledger is complete when all opened channels are closed.
A payload’s provenance is complete when:
A minimal verifier needs only:
1. Read binary prefix, extract: sig_scheme, sig_size, hash_block_size, public_key
2. Verify header signature over the binary prefix bytes
3. Set prev_sig = header_signature
4. Skip header metadata (read 4-byte length, skip that many bytes)
5. For each record:
a. Read record_type (1 byte)
b. Read prev_sig_field (sig_size bytes) — must equal prev_sig
c. If record_type != 0x01: read open_sig (sig_size bytes)
d. Read payload_size (8 bytes, signed int64)
e. If payload_size != 0: read hash_block (hash_block_size bytes)
f. Reconstruct signature input from fields read in (a–e)
g. Read record_signature (sig_size bytes)
h. Verify record_signature against signature input using public_key
i. Set prev_sig = record_signature
j. Read schema_index (1 byte)
k. If schema_index != 255: read metadata_length (4 bytes), skip that many bytes
6. If all signatures verify: ledger is valid
Note: The verifier does not need a CBOR parser, knowledge of hash algorithm names, or awareness of metadata schemas. It operates entirely on the deterministic byte layout.
If a network request fails (timeout, connection reset, server error):
Given: ed25519-sha512 scheme, default hash set [blake2b_256, sha256, sha1, md5].
Header parameters:
| Record Type | Fixed Size |
|---|---|
| Open (payload=0) | 1 + 64 + 8 + 64 + 1 = 138 bytes |
| Open (payload≠0) | 1 + 64 + 8 + 100 + 64 + 1 = 238 bytes |
| Checkpoint/Close/Artifact (payload=0) | 1 + 64 + 64 + 8 + 64 + 1 = 202 bytes |
| Checkpoint/Close/Artifact (payload≠0) | 1 + 64 + 64 + 8 + 100 + 64 + 1 = 302 bytes |
Metadata adds: 4 bytes (length) + CBOR content length.