cortex_verifier/witness.rs
1//! Independent witness types for the trusted evidence reducer.
2//!
3//! Each witness carries:
4//!
5//! - its `class` (which of the four ADR 0041 §4 witness classes it is),
6//! - its `authority_domain` (a disjointness tag, ADR 0013),
7//! - its `tier` (third-party, operator-owned, or local),
8//! - a timestamp (`asserted_at`) and the digest it claims to witness
9//! (`asserted_subject_blake3`),
10//! - an in-memory `signature` payload (no I/O on the trust path),
11//! - a typed `payload` discriminated by `class`.
12//!
13//! No method on these types performs I/O, network, or filesystem reads. The
14//! CLI is responsible for loading the witness bytes and the verifier public
15//! key into memory before invoking [`crate::verify`].
16
17use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19
20/// One of the four ADR 0041 §4 witness classes. Each class binds a fixed
21/// `AuthorityDomain`; mismatches are rejected as `verifier.witness.authority_overlap`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum WitnessClass {
25 /// Signed Cortex ledger chain head (ADR 0022 envelope + Ed25519 head signature).
26 SignedLedgerChainHead,
27 /// External anchor crossing line from a disjoint-authority sink
28 /// (Mechanism A branch-protected, Mechanism B WORM, etc.).
29 ExternalAnchorCrossing,
30 /// Remote-CI conclusion blob signed by the CI provider's key.
31 RemoteCiConclusion,
32 /// SLSA-shaped reproducible-build provenance signed by a builder
33 /// identity distinct from the CI signer.
34 ReproducibleBuildProvenance,
35}
36
37impl WitnessClass {
38 /// Authority domain a well-formed witness of this class MUST advertise.
39 /// Per ADR 0013 §"Invariant: what 'external' means".
40 #[must_use]
41 pub const fn required_authority_domain(self) -> AuthorityDomain {
42 match self {
43 Self::SignedLedgerChainHead => AuthorityDomain::LocalSignedLedger,
44 Self::ExternalAnchorCrossing => AuthorityDomain::ExternalAnchorSink,
45 Self::RemoteCiConclusion => AuthorityDomain::RemoteCiProvider,
46 Self::ReproducibleBuildProvenance => AuthorityDomain::ReproducibleBuildProvider,
47 }
48 }
49
50 /// Stable lowercase wire string for this class. Used in
51 /// `Broken { edge: { detail } }` output.
52 #[must_use]
53 pub const fn wire_str(self) -> &'static str {
54 match self {
55 Self::SignedLedgerChainHead => "signed_ledger_chain_head",
56 Self::ExternalAnchorCrossing => "external_anchor_crossing",
57 Self::RemoteCiConclusion => "remote_ci_conclusion",
58 Self::ReproducibleBuildProvenance => "reproducible_build_provenance",
59 }
60 }
61}
62
63/// Disjointness tag for witness authority. Two witnesses sharing one of these
64/// variants cannot corroborate one another (ADR 0013 §"Invariant: what
65/// 'external' means" and ADR 0040 §"composition boundary").
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum AuthorityDomain {
69 /// Operator-controlled local signing key.
70 LocalSignedLedger,
71 /// External anchor sink with disjoint write authority.
72 ExternalAnchorSink,
73 /// Third-party CI provider's OIDC / signing identity.
74 RemoteCiProvider,
75 /// Third-party reproducible-build provider's signing identity.
76 ReproducibleBuildProvider,
77}
78
79impl AuthorityDomain {
80 /// Stable lowercase wire string for this domain.
81 #[must_use]
82 pub const fn wire_str(self) -> &'static str {
83 match self {
84 Self::LocalSignedLedger => "local_signed_ledger",
85 Self::ExternalAnchorSink => "external_anchor_sink",
86 Self::RemoteCiProvider => "remote_ci_provider",
87 Self::ReproducibleBuildProvider => "reproducible_build_provider",
88 }
89 }
90}
91
92/// Trust tier of the witness signer. Some witness classes require
93/// `ThirdParty` to satisfy ADR 0041 §"Tier sufficiency".
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum WitnessTier {
97 /// Local-only key (operator's own machine, not externally attested).
98 Local,
99 /// Operator-controlled key with declared scope, but not third-party.
100 OperatorOwned,
101 /// A third party signed under their own identity (e.g. GitHub OIDC,
102 /// SLSA builder).
103 ThirdParty,
104}
105
106impl WitnessTier {
107 /// Stable lowercase wire string for this tier.
108 #[must_use]
109 pub const fn wire_str(self) -> &'static str {
110 match self {
111 Self::Local => "local",
112 Self::OperatorOwned => "operator_owned",
113 Self::ThirdParty => "third_party",
114 }
115 }
116}
117
118/// Algorithm-discriminated signature payload for an independent witness.
119///
120/// ## Serialization and backward compatibility
121///
122/// New records are serialized with `#[serde(tag = "type")]`, producing JSON
123/// of the form `{"type": "ed25519", "public_key_bytes": "...", ...}`.
124///
125/// Existing serialized records in the JSONL ledger from before this enum was
126/// introduced are bare structs with no `"type"` field:
127/// `{"verifying_key": "...", "signature": "...", "signer_id": null}`.
128///
129/// The custom [`Deserialize`] implementation tries the tagged form first; on
130/// failure it falls back to [`LegacyEd25519Shape`], which accepts the old
131/// field names and maps them to the `Ed25519` variant. No migration of
132/// existing records is required — they round-trip transparently.
133///
134/// When either migration is triggered (a new witness class lands that cannot
135/// be expressed via the adapter-boundary translation in
136/// `crates/cortex-ledger/src/external_sink/rekor.rs`) or the verifier core
137/// needs to reason uniformly over algorithm families, re-evaluate whether the
138/// custom deserializer can be replaced by a `#[serde(untagged)]` fallback
139/// combining the enum with the legacy shape.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
141#[serde(tag = "type", rename_all = "snake_case")]
142pub enum WitnessSignature {
143 /// Ed25519 signature — current Rekor SET shape. The default variant;
144 /// existing serialized records deserialize as this without migration.
145 Ed25519 {
146 /// 32-byte Ed25519 verifying key (hex-encoded in JSON).
147 #[serde(with = "hex_bytes_32")]
148 public_key_bytes: [u8; 32],
149 /// 64-byte Ed25519 signature over the canonical witness preimage
150 /// (see [`WitnessPayload::canonical_preimage`]).
151 #[serde(with = "hex_bytes_64")]
152 signature_bytes: [u8; 64],
153 /// Optional human-readable signer label (e.g.
154 /// `"https://token.actions.githubusercontent.com"`). Carried for
155 /// reporting; not part of the trust decision.
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 signer_id: Option<String>,
158 },
159 /// ECDSA P-256 signature — for Cosign/Sigstore artifacts. Verification is
160 /// not yet implemented at the verifier layer; the Rekor live adapter
161 /// handles P-256 verification at the adapter boundary
162 /// (`crates/cortex-ledger/src/external_sink/rekor.rs`).
163 EcdsaP256 {
164 /// DER-encoded public key.
165 #[serde(with = "hex_vec")]
166 public_key_der: Vec<u8>,
167 /// DER-encoded signature.
168 #[serde(with = "hex_vec")]
169 signature_der: Vec<u8>,
170 },
171 /// Operator self-signed witness — third authority axis that does not
172 /// require Rekor or OTS. Enables N≥2-distinct-operators quorum for
173 /// operators who cannot use public infrastructure.
174 SelfSigned {
175 /// Operator key identifier (from the authority_key_timeline).
176 key_id: String,
177 /// Raw signature bytes (algorithm determined by key_id lookup).
178 #[serde(with = "hex_vec")]
179 signature_bytes: Vec<u8>,
180 },
181}
182
183impl WitnessSignature {
184 /// Optional signer label for reporting. Only present for the `Ed25519`
185 /// variant; other variants carry identity via their own fields.
186 #[must_use]
187 pub fn signer_id(&self) -> Option<&str> {
188 match self {
189 Self::Ed25519 { signer_id, .. } => signer_id.as_deref(),
190 Self::EcdsaP256 { .. } | Self::SelfSigned { .. } => None,
191 }
192 }
193}
194
195// --- backward-compatible Deserialize ---------------------------------------
196//
197// Strategy: deserialize into a raw `serde_json::Value`, then try the tagged
198// form. If the `"type"` key is absent (legacy bare struct), map the old
199// `verifying_key` / `signature` / `signer_id` fields to `Ed25519`.
200
201impl<'de> Deserialize<'de> for WitnessSignature {
202 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
203 let raw = serde_json::Value::deserialize(deserializer)?;
204
205 // If the value has a "type" field, delegate to the tagged inner enum.
206 if raw.get("type").is_some() {
207 let inner: WitnessSignatureInner =
208 serde_json::from_value(raw).map_err(serde::de::Error::custom)?;
209 return Ok(Self::from(inner));
210 }
211
212 // Legacy bare struct: `verifying_key` (32-byte hex), `signature`
213 // (64-byte hex), `signer_id` (optional string). Produced by the
214 // pre-enum `WitnessSignature` struct before ADR 0041 amendment.
215 let legacy: LegacyEd25519Shape =
216 serde_json::from_value(raw).map_err(serde::de::Error::custom)?;
217 Ok(Self::Ed25519 {
218 public_key_bytes: legacy.verifying_key,
219 signature_bytes: legacy.signature,
220 signer_id: legacy.signer_id,
221 })
222 }
223}
224
225// We cannot derive `Deserialize` directly on `WitnessSignature` with
226// `#[serde(tag)]` because doing so would conflict with our custom impl above.
227// Instead we mirror the three variants in a private inner enum that does
228// derive `Deserialize` through the proc macro, then convert into the public
229// enum.
230
231#[derive(Deserialize)]
232#[serde(tag = "type", rename_all = "snake_case")]
233enum WitnessSignatureInner {
234 Ed25519 {
235 #[serde(with = "hex_bytes_32")]
236 public_key_bytes: [u8; 32],
237 #[serde(with = "hex_bytes_64")]
238 signature_bytes: [u8; 64],
239 #[serde(default)]
240 signer_id: Option<String>,
241 },
242 EcdsaP256 {
243 #[serde(with = "hex_vec")]
244 public_key_der: Vec<u8>,
245 #[serde(with = "hex_vec")]
246 signature_der: Vec<u8>,
247 },
248 SelfSigned {
249 key_id: String,
250 #[serde(with = "hex_vec")]
251 signature_bytes: Vec<u8>,
252 },
253}
254
255impl From<WitnessSignatureInner> for WitnessSignature {
256 fn from(inner: WitnessSignatureInner) -> Self {
257 match inner {
258 WitnessSignatureInner::Ed25519 {
259 public_key_bytes,
260 signature_bytes,
261 signer_id,
262 } => Self::Ed25519 {
263 public_key_bytes,
264 signature_bytes,
265 signer_id,
266 },
267 WitnessSignatureInner::EcdsaP256 {
268 public_key_der,
269 signature_der,
270 } => Self::EcdsaP256 {
271 public_key_der,
272 signature_der,
273 },
274 WitnessSignatureInner::SelfSigned {
275 key_id,
276 signature_bytes,
277 } => Self::SelfSigned {
278 key_id,
279 signature_bytes,
280 },
281 }
282 }
283}
284
285/// Pre-enum (legacy) bare `WitnessSignature` shape for backward-compatible
286/// deserialization of records written before the enum migration.
287#[derive(Deserialize)]
288struct LegacyEd25519Shape {
289 #[serde(with = "hex_bytes_32")]
290 verifying_key: [u8; 32],
291 #[serde(with = "hex_bytes_64")]
292 signature: [u8; 64],
293 #[serde(default)]
294 signer_id: Option<String>,
295}
296
297/// Typed witness payload, discriminated by class.
298///
299/// Each variant is the bag of fields that this witness class must convey.
300/// The fields are deliberately minimal — only what the verifier needs to
301/// reduce the input.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(tag = "kind", rename_all = "snake_case")]
304pub enum WitnessPayload {
305 /// Signed Cortex ledger chain head per ADR 0022.
306 SignedLedgerChainHead {
307 /// Hex-encoded chain head event hash (BLAKE3 over canonical bytes).
308 chain_head_hash: String,
309 /// Position of the chain head row (event count at signing time).
310 event_count: u64,
311 },
312 /// External anchor crossing line from a Mechanism A / B / C sink.
313 ExternalAnchorCrossing {
314 /// Hex-encoded `chain_head_hash` the anchor witnessed.
315 chain_head_hash: String,
316 /// Crossing event count per ADR 0013 anchor payload.
317 event_count: u64,
318 /// Sink kind identifier (e.g. `"branch_protection"`, `"worm_append"`).
319 sink_kind: String,
320 },
321 /// Remote CI conclusion: workflow id + commit SHA + conclusion timestamp.
322 RemoteCiConclusion {
323 /// Workflow run identifier (e.g. GitHub Actions run id as decimal).
324 workflow_run_id: String,
325 /// Git commit SHA whose CI run produced this conclusion.
326 commit_sha: String,
327 /// Conclusion timestamp as ISO 8601 (informational; freshness uses
328 /// [`IndependentWitness::asserted_at`]).
329 conclusion_timestamp: DateTime<Utc>,
330 },
331 /// SLSA-shaped reproducible-build provenance.
332 ReproducibleBuildProvenance {
333 /// Builder identity string, distinct from the remote CI signer.
334 builder_id: String,
335 /// Hex-encoded source digest (input bytes the builder consumed).
336 source_digest: String,
337 /// Hex-encoded output artifact digest.
338 artifact_digest: String,
339 },
340}
341
342impl WitnessPayload {
343 /// Class this payload belongs to. Used to verify the declared `class`
344 /// field against the payload shape.
345 #[must_use]
346 pub const fn class(&self) -> WitnessClass {
347 match self {
348 Self::SignedLedgerChainHead { .. } => WitnessClass::SignedLedgerChainHead,
349 Self::ExternalAnchorCrossing { .. } => WitnessClass::ExternalAnchorCrossing,
350 Self::RemoteCiConclusion { .. } => WitnessClass::RemoteCiConclusion,
351 Self::ReproducibleBuildProvenance { .. } => WitnessClass::ReproducibleBuildProvenance,
352 }
353 }
354
355 /// Canonical signing preimage. Caller signs this exact byte sequence with
356 /// the witness's verifying key; the verifier reconstructs and re-checks it.
357 ///
358 /// The format is a fixed UTF-8 line layout, prefixed with the class wire
359 /// string, the authority domain wire string, the asserted-subject digest,
360 /// and the ISO 8601 `asserted_at` stamp. This is enough to bind the
361 /// signature to the exact tuple `(class, domain, subject, time, payload)`.
362 #[must_use]
363 pub fn canonical_preimage(
364 &self,
365 domain: AuthorityDomain,
366 asserted_subject_blake3: &str,
367 asserted_at: DateTime<Utc>,
368 ) -> Vec<u8> {
369 let mut out = Vec::with_capacity(256);
370 out.extend_from_slice(b"cortex.verifier.witness.v1\n");
371 out.extend_from_slice(b"class=");
372 out.extend_from_slice(self.class().wire_str().as_bytes());
373 out.push(b'\n');
374 out.extend_from_slice(b"authority_domain=");
375 out.extend_from_slice(domain.wire_str().as_bytes());
376 out.push(b'\n');
377 out.extend_from_slice(b"asserted_subject_blake3=");
378 out.extend_from_slice(asserted_subject_blake3.as_bytes());
379 out.push(b'\n');
380 out.extend_from_slice(b"asserted_at=");
381 out.extend_from_slice(asserted_at.to_rfc3339().as_bytes());
382 out.push(b'\n');
383 match self {
384 Self::SignedLedgerChainHead {
385 chain_head_hash,
386 event_count,
387 } => {
388 out.extend_from_slice(b"chain_head_hash=");
389 out.extend_from_slice(chain_head_hash.as_bytes());
390 out.push(b'\n');
391 out.extend_from_slice(b"event_count=");
392 out.extend_from_slice(event_count.to_string().as_bytes());
393 out.push(b'\n');
394 }
395 Self::ExternalAnchorCrossing {
396 chain_head_hash,
397 event_count,
398 sink_kind,
399 } => {
400 out.extend_from_slice(b"chain_head_hash=");
401 out.extend_from_slice(chain_head_hash.as_bytes());
402 out.push(b'\n');
403 out.extend_from_slice(b"event_count=");
404 out.extend_from_slice(event_count.to_string().as_bytes());
405 out.push(b'\n');
406 out.extend_from_slice(b"sink_kind=");
407 out.extend_from_slice(sink_kind.as_bytes());
408 out.push(b'\n');
409 }
410 Self::RemoteCiConclusion {
411 workflow_run_id,
412 commit_sha,
413 conclusion_timestamp,
414 } => {
415 out.extend_from_slice(b"workflow_run_id=");
416 out.extend_from_slice(workflow_run_id.as_bytes());
417 out.push(b'\n');
418 out.extend_from_slice(b"commit_sha=");
419 out.extend_from_slice(commit_sha.as_bytes());
420 out.push(b'\n');
421 out.extend_from_slice(b"conclusion_timestamp=");
422 out.extend_from_slice(conclusion_timestamp.to_rfc3339().as_bytes());
423 out.push(b'\n');
424 }
425 Self::ReproducibleBuildProvenance {
426 builder_id,
427 source_digest,
428 artifact_digest,
429 } => {
430 out.extend_from_slice(b"builder_id=");
431 out.extend_from_slice(builder_id.as_bytes());
432 out.push(b'\n');
433 out.extend_from_slice(b"source_digest=");
434 out.extend_from_slice(source_digest.as_bytes());
435 out.push(b'\n');
436 out.extend_from_slice(b"artifact_digest=");
437 out.extend_from_slice(artifact_digest.as_bytes());
438 out.push(b'\n');
439 }
440 }
441 out
442 }
443}
444
445/// An independent witness, in the shape the verifier consumes. All fields are
446/// already verified at the deserialization boundary: missing keys, malformed
447/// hex, and unknown classes fail closed before [`crate::verify`] runs.
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449pub struct IndependentWitness {
450 /// Witness class.
451 pub class: WitnessClass,
452 /// Authority domain advertised by this witness. MUST equal
453 /// `class.required_authority_domain()` or the verifier rejects with
454 /// `verifier.witness.authority_overlap`.
455 pub authority_domain: AuthorityDomain,
456 /// Trust tier of the signer.
457 pub tier: WitnessTier,
458 /// When the witness was asserted (signed). Compared to the caller-supplied
459 /// `now` for freshness.
460 pub asserted_at: DateTime<Utc>,
461 /// Lowercase BLAKE3 hex of the bytes this witness claims to attest.
462 /// MUST equal the producer-supplied `EvidenceInput::evidence_blake3` or
463 /// the verifier rejects with `verifier.witness.disagreement`.
464 pub asserted_subject_blake3: String,
465 /// Algorithm-discriminated signature material.
466 pub signature: WitnessSignature,
467 /// Typed payload for this witness class.
468 pub payload: WitnessPayload,
469}
470
471impl IndependentWitness {
472 /// Compute the canonical signing preimage this witness's signature must cover.
473 #[must_use]
474 pub fn canonical_preimage(&self) -> Vec<u8> {
475 self.payload.canonical_preimage(
476 self.authority_domain,
477 &self.asserted_subject_blake3,
478 self.asserted_at,
479 )
480 }
481}
482
483/// Lightweight summary returned in `VerifiedTrustState` for reporting.
484/// Contains the typed identity of each witness without leaking signature bytes
485/// into report JSON.
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct WitnessSummary {
488 /// Witness class (wire string).
489 pub class: String,
490 /// Authority domain (wire string).
491 pub authority_domain: String,
492 /// Tier (wire string).
493 pub tier: String,
494 /// Optional signer id (from `WitnessSignature::Ed25519 { signer_id }` or
495 /// the key_id for `SelfSigned`). Not present for `EcdsaP256`.
496 pub signer_id: Option<String>,
497 /// ISO 8601 timestamp when the witness was signed.
498 pub asserted_at: DateTime<Utc>,
499}
500
501impl WitnessSummary {
502 /// Build a summary from an [`IndependentWitness`].
503 #[must_use]
504 pub fn from_witness(witness: &IndependentWitness) -> Self {
505 let signer_id = match &witness.signature {
506 WitnessSignature::Ed25519 { signer_id, .. } => signer_id.clone(),
507 WitnessSignature::SelfSigned { key_id, .. } => Some(key_id.clone()),
508 WitnessSignature::EcdsaP256 { .. } => None,
509 };
510 Self {
511 class: witness.class.wire_str().to_string(),
512 authority_domain: witness.authority_domain.wire_str().to_string(),
513 tier: witness.tier.wire_str().to_string(),
514 signer_id,
515 asserted_at: witness.asserted_at,
516 }
517 }
518}
519
520// ---------------------------------------------------------------------------
521// SelfSignedKeyRegistry — operator-supplied key table for SelfSigned witnesses
522// ---------------------------------------------------------------------------
523
524/// Signature algorithm declared for a `SelfSignedKeyEntry`. The verifier
525/// dispatches on this to select the correct verification routine.
526#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
527#[serde(rename_all = "snake_case")]
528pub enum SelfSignedAlgorithm {
529 /// Ed25519 — 32-byte raw public key.
530 Ed25519,
531 /// ECDSA P-256 — DER-encoded SubjectPublicKeyInfo.
532 EcdsaP256,
533}
534
535/// A single entry in the operator-supplied key registry. Each entry binds a
536/// `key_id` string (matched against `WitnessSignature::SelfSigned { key_id }`)
537/// to an algorithm and the raw public-key bytes.
538#[derive(Debug, Clone, serde::Deserialize)]
539pub struct SelfSignedKeyEntry {
540 /// Identifier matching `WitnessSignature::SelfSigned { key_id }`.
541 pub key_id: String,
542 /// Algorithm family for this key.
543 pub algorithm: SelfSignedAlgorithm,
544 /// Hex-encoded raw public-key bytes.
545 ///
546 /// For `Ed25519`: 32 bytes (64 hex chars).
547 /// For `EcdsaP256`: DER-encoded SubjectPublicKeyInfo.
548 pub key_bytes_hex: String,
549}
550
551impl SelfSignedKeyEntry {
552 /// Decode `key_bytes_hex` into raw bytes.
553 pub fn key_bytes(&self) -> Result<Vec<u8>, String> {
554 hex_decode(&self.key_bytes_hex)
555 }
556}
557
558/// TOML wire shape used only during `SelfSignedKeyRegistry::load()`. The outer
559/// struct matches `{ keys = [...] }`.
560#[derive(serde::Deserialize)]
561struct KeyRegistryFile {
562 keys: Vec<SelfSignedKeyEntry>,
563}
564
565/// In-memory table of operator-supplied public keys for `SelfSigned` witness
566/// verification. Loaded once at CLI startup; the verifier is given a reference
567/// to the populated registry before the trust path runs.
568#[derive(Debug, Clone)]
569pub struct SelfSignedKeyRegistry {
570 entries: Vec<SelfSignedKeyEntry>,
571}
572
573impl SelfSignedKeyRegistry {
574 /// Return an empty registry (no `SelfSigned` witness will verify).
575 #[must_use]
576 pub fn empty() -> Self {
577 Self {
578 entries: Vec::new(),
579 }
580 }
581
582 /// Load a key registry from a TOML file on disk.
583 ///
584 /// Expected file format:
585 ///
586 /// ```toml
587 /// [[keys]]
588 /// key_id = "my-operator-key"
589 /// algorithm = "ed25519"
590 /// key_bytes_hex = "aabbcc..." # 64 hex chars for Ed25519 (32 bytes)
591 ///
592 /// [[keys]]
593 /// key_id = "my-p256-key"
594 /// algorithm = "ecdsa_p256"
595 /// key_bytes_hex = "..." # DER hex
596 /// ```
597 ///
598 /// Returns `Err` if the file cannot be read or fails to parse as the
599 /// expected TOML shape. Key-bytes are NOT decoded here; decoding happens
600 /// at verification time so a single malformed entry does not block
601 /// loading the rest.
602 pub fn load(path: &std::path::Path) -> Result<Self, String> {
603 let raw = std::fs::read_to_string(path).map_err(|e| {
604 format!(
605 "witness-key-registry: cannot read `{}`: {e}",
606 path.display()
607 )
608 })?;
609 let file: KeyRegistryFile = toml::from_str(&raw).map_err(|e| {
610 format!(
611 "witness-key-registry: `{}` did not parse as expected TOML: {e}",
612 path.display()
613 )
614 })?;
615 Ok(Self { entries: file.keys })
616 }
617
618 /// Look up a key entry by `key_id`. Returns `None` when the id is not
619 /// present in this registry.
620 #[must_use]
621 pub fn get(&self, key_id: &str) -> Option<&SelfSignedKeyEntry> {
622 self.entries.iter().find(|e| e.key_id == key_id)
623 }
624
625 /// Number of entries in the registry.
626 #[must_use]
627 pub fn len(&self) -> usize {
628 self.entries.len()
629 }
630
631 /// Whether the registry contains no entries.
632 #[must_use]
633 pub fn is_empty(&self) -> bool {
634 self.entries.is_empty()
635 }
636}
637
638// ---------------------------------------------------------------------------
639// Serde helpers: fixed-size byte arrays and dynamic vecs, hex-encoded in JSON
640// ---------------------------------------------------------------------------
641
642/// Serde module for 32-byte arrays serialized as lowercase hex strings.
643pub(crate) mod hex_bytes_32 {
644 use serde::{Deserialize, Deserializer, Serializer};
645
646 pub fn serialize<S: Serializer>(value: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error> {
647 serializer.serialize_str(&hex_encode(value))
648 }
649
650 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 32], D::Error> {
651 let text = <String as Deserialize>::deserialize(deserializer)?;
652 let bytes = super::hex_decode(&text).map_err(serde::de::Error::custom)?;
653 if bytes.len() != 32 {
654 return Err(serde::de::Error::custom(format!(
655 "expected 32-byte hex string, got {}",
656 bytes.len()
657 )));
658 }
659 let mut out = [0u8; 32];
660 out.copy_from_slice(&bytes);
661 Ok(out)
662 }
663
664 fn hex_encode(bytes: &[u8]) -> String {
665 let mut out = String::with_capacity(bytes.len() * 2);
666 for b in bytes {
667 out.push(super::hex_nibble(b >> 4));
668 out.push(super::hex_nibble(b & 0x0F));
669 }
670 out
671 }
672}
673
674/// Serde module for 64-byte arrays serialized as lowercase hex strings.
675pub(crate) mod hex_bytes_64 {
676 use serde::{Deserialize, Deserializer, Serializer};
677
678 pub fn serialize<S: Serializer>(value: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error> {
679 serializer.serialize_str(&hex_encode(value))
680 }
681
682 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<[u8; 64], D::Error> {
683 let text = <String as Deserialize>::deserialize(deserializer)?;
684 let bytes = super::hex_decode(&text).map_err(serde::de::Error::custom)?;
685 if bytes.len() != 64 {
686 return Err(serde::de::Error::custom(format!(
687 "expected 64-byte hex string, got {}",
688 bytes.len()
689 )));
690 }
691 let mut out = [0u8; 64];
692 out.copy_from_slice(&bytes);
693 Ok(out)
694 }
695
696 fn hex_encode(bytes: &[u8]) -> String {
697 let mut out = String::with_capacity(bytes.len() * 2);
698 for b in bytes {
699 out.push(super::hex_nibble(b >> 4));
700 out.push(super::hex_nibble(b & 0x0F));
701 }
702 out
703 }
704}
705
706/// Serde module for `Vec<u8>` serialized as lowercase hex strings.
707pub(crate) mod hex_vec {
708 use serde::{Deserialize, Deserializer, Serializer};
709
710 pub fn serialize<S: Serializer>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
711 let mut out = String::with_capacity(value.len() * 2);
712 for b in value {
713 out.push(super::hex_nibble(b >> 4));
714 out.push(super::hex_nibble(b & 0x0F));
715 }
716 serializer.serialize_str(&out)
717 }
718
719 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
720 let text = <String as Deserialize>::deserialize(deserializer)?;
721 super::hex_decode(&text).map_err(serde::de::Error::custom)
722 }
723}
724
725fn hex_nibble(nib: u8) -> char {
726 match nib {
727 0..=9 => (b'0' + nib) as char,
728 10..=15 => (b'a' + nib - 10) as char,
729 _ => unreachable!("nibble fits in 4 bits"),
730 }
731}
732
733fn hex_decode(text: &str) -> Result<Vec<u8>, String> {
734 if !text.len().is_multiple_of(2) {
735 return Err(format!("hex string length {} is not even", text.len()));
736 }
737 let mut out = Vec::with_capacity(text.len() / 2);
738 let bytes = text.as_bytes();
739 let mut i = 0;
740 while i < bytes.len() {
741 let high = hex_value(bytes[i])?;
742 let low = hex_value(bytes[i + 1])?;
743 out.push((high << 4) | low);
744 i += 2;
745 }
746 Ok(out)
747}
748
749fn hex_value(byte: u8) -> Result<u8, String> {
750 match byte {
751 b'0'..=b'9' => Ok(byte - b'0'),
752 b'a'..=b'f' => Ok(byte - b'a' + 10),
753 b'A'..=b'F' => Ok(byte - b'A' + 10),
754 _ => Err(format!("non-hex character {byte:#x?}")),
755 }
756}