Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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