Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/witness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ jobs:
# pass, written or ignored (two per pass, one per log), so a
# copy of it is the one field on a line that tells an idle chain
# from a dead checkpointer. Lines before this change drop it.
# `key_epoch` rides along when the registry serves it: which of
# its keys signed the head (registry_key_history in the same
# response), so a line written after a key rotation can still be
# checked against the right key. Absent, not null, on a registry
# that serves none.
# One transient miss wrote a permanent hole. Each fetch below was a
# single curl: when it failed once the line recorded fetch_failed
# for good, while the witness's own fetch of the same route a
Expand All @@ -112,7 +117,7 @@ jobs:
# shim reads it there).
fetch() { curl -sf --max-time 30 --retry 3 --retry-delay 5 --retry-all-errors "$1"; }
if cps=$(fetch https://1f916.ai/api/checkpoint); then
cpj=$(echo "$cps" | jq -c '{registry_key: .registry_public_key.x, checkpoints: [.checkpoints[] | {id, log, tree_size, root, sig, created_at}]}')
cpj=$(echo "$cps" | jq -c '{registry_key: .registry_public_key.x, checkpoints: [.checkpoints[] | {id, log, tree_size, root, sig, created_at} + (if has("key_epoch") then {key_epoch} else {} end)]}')
else
cpj='{"checkpoints":"fetch_failed"}'
fi
Expand Down
25 changes: 25 additions & 0 deletions migrations/0078_registry_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Registry signing-key epochs (src/registry-keys.ts). One row per key the
-- registry has signed with. Epoch 0 is recorded by the first cron pass from
-- REGISTRY_SEED, after that key verifies the oldest and newest head of each log; every
-- later row carries the rotation statement
-- "1f916.registry-rotate.v1:<epoch>:<old>:<new>:<at>:<final_heads>" and its signatures by
-- the previous key (old_sig) and by its own (new_sig). The same statement is
-- chained in identity_events as a 'registry-rotate' event.
CREATE TABLE IF NOT EXISTS registry_keys (
epoch INTEGER PRIMARY KEY CHECK (epoch >= 0),
public_key TEXT NOT NULL UNIQUE,
activated_at INTEGER NOT NULL,
retired_at INTEGER,
statement TEXT,
old_sig TEXT,
new_sig TEXT,
-- The old epoch's final heads, committed to in the statement: JSON
-- [{log, tree_size, root}] in log order, the newest head of every log when
-- the previous key was retired.
final_heads TEXT,
CHECK ((epoch = 0) = (statement IS NULL))
);

-- The epoch whose key signed each head. Every head before this migration was
-- signed by the one key there was, which is epoch 0.
ALTER TABLE checkpoints ADD COLUMN key_epoch INTEGER NOT NULL DEFAULT 0;
24 changes: 24 additions & 0 deletions schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,8 @@ CREATE TABLE IF NOT EXISTS checkpoints (
root TEXT NOT NULL,
sig TEXT NOT NULL,
created_at INTEGER NOT NULL,
-- migrations/0078: the registry key epoch that signed this head.
key_epoch INTEGER NOT NULL DEFAULT 0,
UNIQUE(log, tree_size)
);
CREATE INDEX IF NOT EXISTS idx_checkpoints_log ON checkpoints(log, id DESC);
Expand Down Expand Up @@ -1676,3 +1678,25 @@ CREATE TABLE IF NOT EXISTS checkpoint_cosignatures (
PRIMARY KEY (checkpoint_id, witness, key_id)
);
CREATE INDEX IF NOT EXISTS idx_checkpoint_cosignatures_witness ON checkpoint_cosignatures(log, witness, key_id, checkpoint_id);

-- migrations/0078: registry signing-key epochs (src/registry-keys.ts). One row per key the
-- registry has signed with. Epoch 0 is recorded by the first cron pass from
-- REGISTRY_SEED, after that key verifies the oldest and newest head of each log; every
-- later row carries the rotation statement
-- "1f916.registry-rotate.v1:<epoch>:<old>:<new>:<at>:<final_heads>" and its signatures by
-- the previous key (old_sig) and by its own (new_sig). The same statement is
-- chained in identity_events as a 'registry-rotate' event.
CREATE TABLE IF NOT EXISTS registry_keys (
epoch INTEGER PRIMARY KEY CHECK (epoch >= 0),
public_key TEXT NOT NULL UNIQUE,
activated_at INTEGER NOT NULL,
retired_at INTEGER,
statement TEXT,
old_sig TEXT,
new_sig TEXT,
-- The old epoch's final heads, committed to in the statement: JSON
-- [{log, tree_size, root}] in log order, the newest head of every log when
-- the previous key was retired.
final_heads TEXT,
CHECK ((epoch = 0) = (statement IS NULL))
);
8 changes: 8 additions & 0 deletions schemas/checkpoint-consistency.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@
"type": "string",
"minLength": 1,
"description": "Server-authored verification recipe. Presence is the contract; wording is not pinned."
},
"registry_key_history": {
"type": "array",
"minItems": 1,
"items": {
"type": "object"
},
"description": "The registry's signing keys by epoch, with the rotation statements that link them, as GET /api/checkpoint serves it (schemas/checkpoint.json registry_key_history). Served so this file alone tells a verifier which key checks each head it carries. Absent on a deployment with no registry key configured."
}
},
"$defs": {
Expand Down
113 changes: 113 additions & 0 deletions schemas/checkpoint.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@
"type": "integer",
"minimum": 0,
"description": "Ms epoch when this checkpoint was signed"
},
"key_epoch": {
"type": "integer",
"minimum": 0,
"description": "The registry key epoch whose key signed this head (registry_key_history). Absent only on a registry that predates key epochs, where every head is epoch 0."
}
}
}
Expand Down Expand Up @@ -431,6 +436,114 @@
"type": "string",
"minLength": 1,
"description": "Present with cosignatures: how a cosignature line is built and checked."
},
"registry_key_epoch": {
"type": "integer",
"minimum": 0,
"description": "Epoch of registry_public_key, the active key. Optional in this schema so a deployment that predates key epochs still validates; served on every response since migration 0078."
},
"registry_key_history": {
"type": "array",
"minItems": 1,
"description": "Every key this registry has signed with, by epoch, oldest first. Epoch 0 has rotation null; every later epoch carries the statement in rotation_statement_format signed by the previous key (old_sig) and its own (new_sig). A verifier that pinned any key in the list follows the statements from it.",
"items": {
"type": "object",
"required": [
"epoch",
"public_key",
"activated_at",
"retired_at",
"rotation"
],
"properties": {
"epoch": {
"type": "integer",
"minimum": 0
},
"public_key": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]{43}$",
"description": "Base64url raw Ed25519 public key of this epoch"
},
"activated_at": {
"type": "integer",
"description": "Epoch milliseconds the key became active; 0 for epoch 0, which was active from the start of the log"
},
"retired_at": {
"type": [
"integer",
"null"
],
"description": "When the next epoch activated; null for the active key. A head naming this epoch must have created_at before it."
},
"rotation": {
"type": [
"object",
"null"
],
"required": [
"statement",
"old_sig",
"new_sig",
"final_heads"
],
"properties": {
"statement": {
"type": "string",
"pattern": "^1f916\\.registry-rotate\\.v1:[0-9]+:[A-Za-z0-9_-]{43}:[A-Za-z0-9_-]{43}:[0-9]+:([a-z_]+=[0-9]+=[0-9a-f]{64}(,[a-z_]+=[0-9]+=[0-9a-f]{64})*)?$"
},
"old_sig": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]+$"
},
"new_sig": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]+$"
},
"final_heads": {
"type": "array",
"description": "The previous epoch's final heads: every log's newest head when that key was retired, in log order, as the statement's last field carries them. A head of the retired epoch past these (a larger tree_size, or another root at the same size) is refused whatever its created_at; below them it counts only with a consistency proof to them.",
"items": {
"type": "object",
"required": [
"log",
"tree_size",
"root"
],
"properties": {
"log": {
"type": "string"
},
"tree_size": {
"type": "integer",
"minimum": 0
},
"root": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
}
}
}
}
}
}
},
"registry_key_history_recorded": {
"type": "boolean",
"description": "false: nothing recorded yet (the first cron pass after migration 0078 records epoch 0), and the one history row is epoch 0 derived from the configured key"
},
"registry_key_history_has_more": {
"type": "boolean",
"description": "true when more epochs exist than one response serves: the newest are served, ending at the active key, and the oldest are left out"
},
"rotation_statement_format": {
"type": "string",
"const": "1f916.registry-rotate.v1:<epoch>:<old_public_key>:<new_public_key>:<at>:<log>=<tree_size>=<root>[,<log>=<tree_size>=<root>...]"
},
"registry_key_note": {
"type": "string"
}
},
"$id": "https://1f916.ai/schemas/checkpoint.json",
Expand Down
1 change: 1 addition & 0 deletions schemas/events-paged.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
"key-rotate",
"witness-register",
"witness-rotate",
"registry-rotate",
"flag-disposition",
"payout-binding",
"payout-wallet",
Expand Down
1 change: 1 addition & 0 deletions schemas/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@
"key-rotate",
"witness-register",
"witness-rotate",
"registry-rotate",
"flag-disposition",
"payout-binding",
"payout-wallet",
Expand Down
36 changes: 36 additions & 0 deletions schemas/proof.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,42 @@
"type": "string",
"minLength": 1,
"description": "Server-authored verification recipe. Presence is the contract; wording is not pinned."
},
"registry_key_history": {
"type": "array",
"minItems": 1,
"items": {
"type": "object"
},
"description": "The registry's signing keys by epoch, with the rotation statements that link them, as GET /api/checkpoint serves it (schemas/checkpoint.json registry_key_history). Served so this file alone tells a verifier which key checks each head it carries. Absent on a deployment with no registry key configured."
},
"final_consistency": {
"type": "object",
"description": "Present when the covering checkpoint was signed by a registry key since retired and is smaller than that key's committed final head for this log (registry_key_history rotation.final_heads): the RFC 6962 consistency proof from this checkpoint to that final head. verify.mjs accepts such a head only with it.",
"required": [
"log",
"from",
"to",
"proof"
],
"properties": {
"log": {
"type": "string"
},
"from": {
"type": "object"
},
"to": {
"type": "object"
},
"proof": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
}
}
},
"$defs": {
Expand Down
21 changes: 21 additions & 0 deletions schemas/record.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@
"type": "string",
"description": "Base64url 32-byte Ed25519 registry key.",
"pattern": "^[A-Za-z0-9_-]{43}$"
},
"key_epoch": {
"type": "integer",
"minimum": 0,
"description": "Epoch of the registry key that signed this dossier (GET /api/checkpoint registry_key_history)"
}
}
},
Expand Down Expand Up @@ -554,6 +559,22 @@
"type": "string",
"description": "Disclosure of how the 200-row caps work and where to read the rest.",
"minLength": 1
},
"checkpoint_key_epoch": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Epoch of the key that signed `checkpoint`, served outside the signed core; null when there is no checkpoint. A head signed before a rotation names the older epoch."
},
"registry_key_history": {
"type": "array",
"minItems": 1,
"items": {
"type": "object"
},
"description": "The registry's signing keys by epoch, with the rotation statements that link them, as GET /api/checkpoint serves it (schemas/checkpoint.json registry_key_history). Served so this file alone tells a verifier which key checks each head it carries. Absent on a deployment with no registry key configured."
}
},
"allOf": [
Expand Down
Loading
Loading