oc_crypto/lib.rs
1//! Cryptographic core of the `.cc` container.
2//!
3//! A crate-wide, lint-enforced rule: **no I/O,
4//! clocks, or randomness from thin air**. Nonces and RNGs are passed
5//! as arguments. This makes every test deterministic, while Wycheproof vectors
6//! exercise our own call sites, not merely the underlying crates.
7//!
8//! Parsing ensures that plaintext never leaves the function before the authentication
9//! tag is checked: on failure the buffer is wiped, and the caller must treat it
10//! as unusable.
11
12pub mod aead;
13pub mod agreement;
14pub mod kdf;
15pub mod mac;
16pub mod merkle;
17pub mod mlkem_p256;
18pub mod rsa;
19/// Software RSA-PSS signer: only for probes and the testbed (`docs/format.md`,
20/// "EDITING IS EXECUTABLE"). Excluded from production: the `test-signer` feature.
21#[cfg(any(test, feature = "test-signer"))]
22pub mod rsa_test_signer;
23pub mod seal;
24pub mod secret;
25pub mod sign;
26/// Payload chunking discipline: one loop for every host.
27pub mod stream;
28pub mod tpm;
29pub mod transcript;
30pub mod wrap;
31pub mod xwing;
32
33pub use label::Label;
34pub use secret::{
35 Cek, ClaimSecret, Kek, MacKey, MetaKey, PayloadKey, SecretA, SecretB, SecretBuf, X25519Secret,
36};
37pub use transcript::Transcript;
38
39/// Cryptographic operation errors.
40///
41/// Variants deliberately reveal few details: an error message must not give
42/// an adversary another bit of information about precisely which check
43/// failed or at which byte.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum CryptoError {
46 /// Authentication failed: AEAD tag, MAC, or slot commitment.
47 /// One variant for all three cases, deliberately.
48 Authentication,
49 /// The signature is invalid or noncanonical.
50 BadSignature,
51 /// Input or output length violates the contract.
52 BadLength,
53 /// Data does not match the tree root.
54 TreeMismatch,
55 /// Access outside the tree.
56 IndexOutOfRange,
57 /// The key is not a valid curve point or is forbidden (small order).
58 BadKey,
59 /// This client build does not support the algorithm identifier.
60 UnsupportedAlgorithm,
61}
62
63impl core::fmt::Display for CryptoError {
64 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65 let text = match self {
66 Self::Authentication => "проверка подлинности не прошла",
67 Self::BadSignature => "подпись неверна",
68 Self::BadLength => "некорректная длина данных",
69 Self::TreeMismatch => "данные не соответствуют дереву целостности",
70 Self::IndexOutOfRange => "индекс за пределами дерева",
71 Self::BadKey => "некорректный ключ",
72 Self::UnsupportedAlgorithm => "алгоритм не поддерживается",
73 };
74 f.write_str(text)
75 }
76}
77
78impl core::error::Error for CryptoError {}
79
80/// Algorithm identifiers covered by the header signature.
81///
82/// Agility uses explicit numbers rather than "the current best choice" because
83/// the KEM will change during the product's lifetime: X25519 will give way to an ML-KEM hybrid,
84/// and slots using different KEMs must coexist in one file.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[repr(u8)]
87pub enum AeadAlg {
88 /// Primary profile. A 192-bit nonce allows storing a random nonce.
89 XChaCha20Poly1305 = 1,
90 /// Profile for FIPS requirements. A 96-bit nonce requires a counter and
91 /// makes the container effectively write-once.
92 Aes256Gcm = 2,
93 /// Nonce-reuse-resistant variant.
94 ///
95 /// This used to say "for editable files", which was a promise, not a
96 /// description: editable files use `aead_id = 1`, like all
97 /// others. The property SIV was meant to provide comes from plaintext-hedged
98 /// nonces ([`aead::seal_chunk_hedged`]), introduced after
99 /// the promise itself and making the second profile unnecessary. See
100 /// `docs/format.md` §6.1.
101 ///
102 /// The member remains to prevent assigning number 3 to another cipher.
103 Aes256GcmSiv = 3,
104}
105
106impl AeadAlg {
107 /// Parse an identifier from a file. Unknown values cause rejection, not
108 /// substitution of a default.
109 ///
110 /// Parsing immediately passes the second boundary, [`aead::ensure_supported`],
111 /// just as [`TreeHashAlg::from_u8`] already does. The asymmetry was
112 /// substantive: a header with `aead_id = 2` passed signature verification, slot parsing,
113 /// key agreement, and CEK unwrapping before failing on the first chunk. All
114 /// that work was done on a file already known, at parsing time,
115 /// to be impossible to open.
116 pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
117 let alg = match v {
118 1 => Self::XChaCha20Poly1305,
119 2 => Self::Aes256Gcm,
120 3 => Self::Aes256GcmSiv,
121 _ => return Err(CryptoError::UnsupportedAlgorithm),
122 };
123 aead::ensure_supported(alg)?;
124 Ok(alg)
125 }
126}
127
128/// Signature algorithm.
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130#[repr(u8)]
131pub enum SigAlg {
132 /// Author signature. The only one this build executes.
133 Ed25519 = 1,
134 /// Editing-device signature, format version 3: RSA-PSS-SHA256,
135 /// MGF1-SHA256, 32-byte salt, exponent 65537.
136 ///
137 /// Chosen for its failure mode, not taste: ECDSA consumes an ephemeral `k` for
138 /// every signature, and signing two different messages with the same `k` reveals
139 /// the private key by arithmetic, exactly what virtual-machine snapshot
140 /// rollback causes. With PSS, repeated salt yields one extra valid signature and
141 /// nothing more. See `docs/format.md`, "VERSION 3 OPENED", item 7.
142 ///
143 /// Verifier: [`rsa::verify_pss_sha256`], in pure Rust: `oc-format`
144 /// verifies the signature and must build for
145 /// `wasm32-unknown-unknown`.
146 ///
147 /// Used **only by mutable-region tag 6**. This number is invalid in `suite.sig_alg`:
148 /// that field specifies the AUTHOR signature, frozen in version 1 as
149 /// Ed25519. Header parsing checks placement: `ensure_supported`
150 /// answers "can we execute it", not "does it belong here".
151 RsaPssSha256 = 2,
152}
153
154impl SigAlg {
155 /// Whether the build can execute the declared algorithm.
156 ///
157 /// A second boundary, like [`aead::ensure_supported`] and
158 /// [`merkle::ensure_supported`]. Without it, an identifier in a signed
159 /// header controls nothing: a file declaring RSA-PSS would still
160 /// be verified as Ed25519 and accepted. This is the same defect class
161 /// that produced `alg: none` in JWS.
162 ///
163 /// The `match` deliberately has no `_`: adding a member must break the build here,
164 /// beside verification, rather than pass silently.
165 pub fn ensure_supported(self) -> Result<(), CryptoError> {
166 match self {
167 Self::Ed25519 => Ok(()),
168 // Исполняется с появлением `rsa::verify_pss_sha256`. До него здесь
169 // стоял отказ, и это было верно: номер, который сборка не умеет
170 // исполнить, обязан отвергаться на разборе.
171 Self::RsaPssSha256 => Ok(()),
172 }
173 }
174
175 /// Parse an identifier from a file.
176 ///
177 /// As with [`AeadAlg::from_u8`], parsing immediately passes through
178 /// [`Self::ensure_supported`]: a header using an unimplemented algorithm must
179 /// be rejected AT PARSING, not after key agreement and CEK unwrapping.
180 pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
181 let alg = match v {
182 1 => Self::Ed25519,
183 2 => Self::RsaPssSha256,
184 _ => return Err(CryptoError::UnsupportedAlgorithm),
185 };
186 alg.ensure_supported()?;
187 Ok(alg)
188 }
189}
190
191/// Key encapsulation mechanism. Specified **per slot**, not per file.
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193#[repr(u8)]
194pub enum KemAlg {
195 /// Author → server and author → recipient.
196 X25519HkdfSha256 = 1,
197 /// Server → device. P-256 specifically, not X25519: Microsoft Platform Crypto
198 /// Provider does not provide X25519; TPM 2.0 offers ECDH P-256 and RSA.
199 P256HkdfSha256 = 2,
200 /// Fallback for TPMs without ECDH support.
201 RsaOaepSha256 = 3,
202 /// X25519 and ML-KEM-768 hybrid using X-Wing: a post-quantum half
203 /// alongside a classical half. Mechanism: [`crate::xwing`]; normative form:
204 /// `docs/format.md`, "VERSION 3 OPENED", item 1 (the item itself is in version 4).
205 ///
206 /// Targets a SOFTWARE key: X-Wing is defined over X25519, while a TPM key
207 /// is P-256. Post-quantum protection and hardware binding of the recipient are currently
208 /// mutually exclusive; see `docs/threat-model.md` §2.
209 XWing = 4,
210 /// ECDH P-256 and ML-KEM-768 hybrid: MLKEM768-P256. Mechanism:
211 /// [`crate::mlkem_p256`]; normative form: `docs/format.md`, version 5.
212 ///
213 /// The difference from [`Self::XWing`] is not strength but WHERE the classical
214 /// half resides: Platform Crypto Provider supports P-256 but not X25519. Thus
215 /// the fifth mechanism is the only one combining post-quantum protection with
216 /// TPM key non-exportability rather than making them mutually exclusive.
217 MlKem768P256 = 5,
218}
219
220impl KemAlg {
221 /// Whether the build can execute the declared mechanism: ONE name for this question.
222 ///
223 /// The name was not introduced for neatness. "Is this mechanism executable?"
224 /// was answered separately by the fingerprint table ([`kdf::device_fpr`]), header length
225 /// tables, and the client's share-B issuance branch; the answers agreed
226 /// only through the editor's memory. Such a set can diverge exactly once:
227 /// in the direction of "somewhere a mechanism this build cannot execute was considered
228 /// executable".
229 ///
230 /// The answer comes from [`seal::supports_kem`] rather than being duplicated here: the `match`
231 /// without `_` must sit BESIDE THE IMPLEMENTATION, so adding a [`KemAlg`]
232 /// member breaks the build at the code responsible for executing it. This is
233 /// the `Result` form of the same answer, for callers needing rejection rather than `bool`,
234 /// following [`SigAlg::ensure_supported`].
235 ///
236 /// # Errors
237 /// [`CryptoError::UnsupportedAlgorithm`]: the registry has the number but the build
238 /// lacks the mechanism.
239 pub fn ensure_supported(self) -> Result<(), CryptoError> {
240 if seal::supports_kem(self) { Ok(()) } else { Err(CryptoError::UnsupportedAlgorithm) }
241 }
242
243 /// Parse an identifier from a file.
244 ///
245 /// # Why this has NO second boundary, unlike its neighbors
246 ///
247 /// [`AeadAlg::from_u8`], [`SigAlg::from_u8`], and [`TreeHashAlg::from_u8`]
248 /// call `ensure_supported` during parsing: an unimplemented header algorithm
249 /// must reject the FILE, the earlier the better. For `kem_id`, the consequence
250 /// differs normatively: `docs/format.md` §3.3 and §3.5 require
251 /// SKIPPING a slot with an unimplemented or unknown mechanism rather than rejecting
252 /// the file; a usable slot for this recipient may be adjacent.
253 /// Rejection built in here would move "skip or reject" from
254 /// the caller's level into number parsing, which knows nothing
255 /// about slots.
256 ///
257 /// The boundary therefore remains a separate [`Self::ensure_supported`] call,
258 /// while parsing answers only "is this number in the registry?".
259 pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
260 match v {
261 1 => Ok(Self::X25519HkdfSha256),
262 2 => Ok(Self::P256HkdfSha256),
263 3 => Ok(Self::RsaOaepSha256),
264 4 => Ok(Self::XWing),
265 5 => Ok(Self::MlKem768P256),
266 _ => Err(CryptoError::UnsupportedAlgorithm),
267 }
268 }
269}
270
271/// Payload-tree hash.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273#[repr(u8)]
274pub enum TreeHashAlg {
275 /// Naturally tree-based and substantially faster than SHA-256.
276 Blake3 = 1,
277 /// Listed in the format registry but not used to compute trees in this build: see
278 /// [`merkle::ensure_supported`]. The member remains to prevent reusing number 2
279 /// for another hash; otherwise old files would be read with the wrong
280 /// algorithm rather than rejected.
281 Sha256 = 2,
282}
283
284impl TreeHashAlg {
285 /// Parse an identifier from a file.
286 ///
287 /// Parsing a number and being able to execute it are distinct; previously this build
288 /// only did the first: it accepted `tree_hash_id = 2`, yet still computed the tree
289 /// using BLAKE3. Parsing therefore immediately passes a second boundary,
290 /// [`merkle::ensure_supported`], the sole declaration of what this
291 /// build supports. A single place prevents the supported-algorithm list from
292 /// diverging from the hasher's behavior.
293 pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
294 let alg = match v {
295 1 => Self::Blake3,
296 2 => Self::Sha256,
297 _ => return Err(CryptoError::UnsupportedAlgorithm),
298 };
299 merkle::ensure_supported(alg)?;
300 Ok(alg)
301 }
302}
303
304/// Domain-separation labels.
305///
306/// None is used twice across the system. The only route into
307/// a signature is through [`Transcript`], whose constructor requires a label.
308pub mod label {
309 /// A domain label: a value that CANNOT be invented.
310 ///
311 /// # Why a type where `&'static [u8]` used to suffice
312 ///
313 /// I-12 requires unique, prefix-free labels, guarded by
314 /// four probes: uniqueness, prefix-freeness, versioning, and agreement with
315 /// specification §3.6. All four inspect [`ALL`], the REGISTRY. They never saw
316 /// call sites: `Transcript::new` and `seal::slot_info`
317 /// accepted arbitrary bytes, and a caller could pass
318 /// `b"CC/v1/lease-cache"`, a string absent from the registry and extending
319 /// [`LEASE`]. No probe would detect that, because it would
320 /// check the list rather than the call.
321 ///
322 /// The new type closes exactly this gap: `Label` values come ONLY
323 /// from this module's constants because the constructor is private and the field
324 /// is not public. The registry's guarantee becomes a guarantee of every call,
325 /// checked by the compiler rather than a probe.
326 ///
327 /// # Why `Debug` prints the string itself
328 ///
329 /// A label is not secret: it is plaintext in every file and in the specification.
330 /// I-11 forbids secrets in `Debug`, not domain names; hiding
331 /// the label would blind signature-failure debugging without the slightest
332 /// benefit.
333 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
334 pub struct Label(&'static [u8]);
335
336 impl Label {
337 /// Create a label. Deliberately private: see the type documentation.
338 ///
339 /// `const fn` because all labels are constants; a constant
340 /// constructed at runtime would require `OnceLock` where all that is needed
341 /// is a byte array.
342 const fn new(bytes: &'static [u8]) -> Self {
343 Self(bytes)
344 }
345
346 /// Label bytes: what enters `info`, the transcript, and the preimage.
347 #[must_use]
348 pub const fn as_bytes(self) -> &'static [u8] {
349 self.0
350 }
351
352 /// Byte length. Needed in `const` context: the private-metadata AAD layout
353 /// is computed at build time (`aead::META_AAD_LEN`).
354 #[must_use]
355 pub const fn len(self) -> usize {
356 self.0.len()
357 }
358
359 /// Whether the label is empty. Always `false`: a label with no bytes cannot separate
360 /// domains, but without this method `clippy::len_without_is_empty` is right.
361 #[must_use]
362 pub const fn is_empty(self) -> bool {
363 self.0.is_empty()
364 }
365
366 /// A label OUTSIDE THE REGISTRY: for prototypes and probes only.
367 ///
368 /// Needed by two out-of-tree prototypes (`experiments/attested-release`,
369 /// `spikes/disclosure-capsules`): each signs ITS OWN statement in
370 /// its own domain (`"F29/proto/evidence"`, `"SS/spike/..."`); adding those
371 /// strings to the registry is forbidden, since the registry is normative and the prototype may die tomorrow.
372 ///
373 /// The `ad-hoc-label` feature is disabled by default and listed as
374 /// non-shipping (`cc-cli/tests/repository_hygiene.rs`) for the same
375 /// reason as `explicit-nonce`: enabling it in a release restores the
376 /// production escape hatch that this type was introduced to close.
377 #[cfg(any(test, feature = "ad-hoc-label"))]
378 #[must_use]
379 pub const fn ad_hoc(bytes: &'static [u8]) -> Self {
380 Self::new(bytes)
381 }
382 }
383
384 impl core::fmt::Debug for Label {
385 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
386 match core::str::from_utf8(self.0) {
387 Ok(text) => write!(f, "Label({text})"),
388 // Метки реестра — ASCII по построению, и эта ветка недостижима
389 // для них. Она существует ради `ad_hoc`, которому байты передают
390 // прототипы.
391 Err(_) => write!(f, "Label({:?})", self.0),
392 }
393 }
394 }
395
396 pub const HEADER_SIG: Label = Label::new(b"CC/v1/header-sig");
397 pub const REVOCATION: Label = Label::new(b"CC/v1/revocation");
398 pub const GRANT: Label = Label::new(b"CC/v1/grant");
399 /// Author signature on an AGENT GRANT: door, expiry, depth, and shares B
400 /// for the subtree (`oc_protocol::agent::AgentGrant`, Agent Protocol, stage 1).
401 ///
402 /// Its own label, not [`GRANT`], for good reason: `"CC/v1/grant"` marks
403 /// author approval of ONE request for one file
404 /// (`oc_protocol::access::decision_transcript`), whereas an agent grant distributes shares
405 /// for an entire tree and names the key with which the door signs delegations.
406 /// If the domains coincided, approval of one file would also serve as a signature
407 /// for distributing the entire subtree.
408 ///
409 /// Prefix-free: `"CC/v1/grant"` is not its prefix (seventh byte `a`
410 /// versus `g`); its nearest `a` neighbors, `"CC/v1/activate-req"`,
411 /// `"CC/v1/audit-entry"`, `"CC/v1/audit-head"`, `"CC/v1/attest-nonce"`,
412 /// `"CC/v1/attest-qualify"`, `"CC/v1/author-order"`,
413 /// `"CC/v1/authority-binding"`, and `"CC/v1/authority-transfer"`, differ
414 /// at byte eight (`g` versus `c`, `u`, `t`).
415 ///
416 /// Does not affect the container: it changes no format version and enters no
417 /// header. It is registered because prefix-freeness can be proved
418 /// only here (I-12).
419 pub const AGENT_GRANT: Label = Label::new(b"CC/v1/agent-grant");
420 /// Parent-door signature on a DELEGATION to a child
421 /// (`oc_protocol::agent::Delegation`, same source).
422 ///
423 /// Distinct from [`AGENT_GRANT`]: the author signs a grant with the container
424 /// header's key, while the door signs delegations with its own ephemeral Ed25519 key. A signature
425 /// on one must not work for the other, or a door receiving
426 /// a grant could issue itself another grant, with new depth and expiry.
427 ///
428 /// Prefix-free: the nearest `d` labels are `"CC/v1/device-fpr"`,
429 /// `"CC/v1/directory-entry"`, and `"CC/v1/directory-head"`, all differing
430 /// at byte eight (`e` versus `e`/`i`; for `device-fpr`, at byte nine,
431 /// `l` versus `v`).
432 pub const DELEGATION: Label = Label::new(b"CC/v1/delegation");
433 /// Author signature on an ACTION GRANT: which actions, subject to which
434 /// constraints, the door may request from the server
435 /// (`oc_protocol::action::ActionGrant`, Agent Protocol, stage 2,
436 /// `docs/agent-protocol/stage-2-actions.md` §4.1).
437 ///
438 /// Distinct from [`AGENT_GRANT`], for the same reason separating an agent grant from
439 /// [`GRANT`]: an agent grant distributes shares B for READING a subtree; an action
440 /// grant distributes permission to ACT outside the sandbox: push a branch,
441 /// delete a file, contact the outside. If domains matched, a signature granting
442 /// read access would grant actions too: an author opening a directory
443 /// to an agent would silently grant it `git push` as well.
444 ///
445 /// Prefix-free: among `a` labels, the closest prefix is `"CC/v1/activate-req"`,
446 /// which differs at the fifth byte of the name (`o` versus `v`:
447 /// `action` versus `activate`); its neighbor [`ACTION_LEASE`] shares only
448 /// `"CC/v1/action-"`, followed by `g` versus `l`. Neither extends the other.
449 ///
450 /// Does not affect the container: it changes no format version, enters no header,
451 /// and none of its bytes occur in `.cc`. It is registered
452 /// because prefix-freeness can be proved only here (I-12).
453 pub const ACTION_GRANT: Label = Label::new(b"CC/v1/action-grant");
454 /// SERVER signature on a single-use action lease
455 /// (`oc_protocol::action::ActionLease`, same source, §4.3).
456 ///
457 /// The key is the same one the server uses to sign file leases
458 /// (`authority.lease_verify_key` from the header), but the label is distinct for a
459 /// reason: a file lease permits OPENING a file; an action lease permits
460 /// EXECUTING an action with specified arguments. If domains matched, one
461 /// signed document could replace the other under the same key, reducing
462 /// the distinction between "read" and "push a branch" to how the recipient
463 /// interprets the bytes.
464 ///
465 /// Prefix-free with [`LEASE`] (`"CC/v1/lease"` is not its prefix) and
466 /// [`ACTION_GRANT`] (see there).
467 pub const ACTION_LEASE: Label = Label::new(b"CC/v1/action-lease");
468 /// AUTHOR signature on a decision concerning an action-execution request
469 /// (`oc_protocol::action::ActionDecision`, stage 2, §5 step 3).
470 ///
471 /// Distinct from [`GRANT`], which signs decisions about ACCESS requests
472 /// (`oc_protocol::access::decision_transcript`). Both use the same key,
473 /// from the header and recorded by the server at file registration;
474 /// only the label separates the domains. If they coincided, two different author
475 /// statements would share one signature: "issue this device the file's share B"
476 /// and "let this door execute `git push` to this branch". Differing tag
477 /// numbers cannot be relied on to separate them: both bodies use
478 /// TLV, and matching numbers are a matter of time, not construction.
479 ///
480 /// Prefix-free: with [`ACTION_GRANT`] and [`ACTION_LEASE`] it shares only
481 /// `"CC/v1/action-"`, then `d` versus `g` and `l`; no registry label
482 /// starts with `"CC/v1/action-d"`. Like both neighbors, it does not affect
483 /// the container: it changes no format version and enters no header.
484 pub const ACTION_DECISION: Label = Label::new(b"CC/v1/action-decision");
485 pub const LEASE: Label = Label::new(b"CC/v1/lease");
486 pub const ACTIVATE_REQ: Label = Label::new(b"CC/v1/activate-req");
487 pub const AUDIT_ENTRY: Label = Label::new(b"CC/v1/audit-entry");
488 /// Signed log head: size and tree root over the entries.
489 ///
490 /// Its own label rather than a shared entry label, for good reason: heads and entries are
491 /// DIFFERENT statements. A signature on one must not work for the other,
492 /// or a signed entry could be presented as a signed head.
493 ///
494 /// Prefix-free: `"CC/v1/audit-entry"` is not its prefix, nor vice versa
495 /// (I-12), tested across the entire list.
496 pub const AUDIT_HEAD: Label = Label::new(b"CC/v1/audit-head");
497 pub const ATTEST_NONCE: Label = Label::new(b"CC/v1/attest-nonce");
498 pub const CONTENT_MAC: Label = Label::new(b"CC/v1/content-mac");
499 /// Editor signature over the mutable region (`docs/format.md`, "EDITING IS
500 /// EXECUTABLE", item C).
501 pub const EDITOR_SIG: Label = Label::new(b"CC/v1/editor-sig");
502 /// Editing-key certificate: the author or a coauthor certifies "device X has
503 /// editing key S for this file" (same source, item B).
504 ///
505 /// Prefix-free with [`EDITOR_SIG`]: they share only `"CC/v1/editor-"`, and
506 /// neither extends the other.
507 pub const EDITOR_CERT: Label = Label::new(b"CC/v1/editor-cert");
508 /// Editing-session head: a hash chain of saves (same source, item D).
509 pub const EDIT_SESSION: Label = Label::new(b"CC/v1/edit-session");
510 /// Revision submission to the server (`docs/protocol.md` §9.12): a separate label so
511 /// an editing-key signature on a submission cannot serve as a region signature,
512 /// or vice versa (I-12).
513 pub const EDITION_CLAIM: Label = Label::new(b"CC/v1/edition-claim");
514 /// File imprint for an RFC 3161 timestamp (`docs/format.md`, "FOOTER AND
515 /// TIMESTAMP", item B).
516 pub const FOOTER_IMPRINT: Label = Label::new(b"CC/v1/footer-imprint");
517 /// Witness signature over the server's log head (`docs/protocol.md`
518 /// §9.13, D3). Separate from `audit-head`: the server signs the head,
519 /// while another party signs the witness statement; one's signature must not serve
520 /// as the other's.
521 pub const WITNESS_COSIGN: Label = Label::new(b"CC/v1/witness-cosign");
522 /// Key-directory log leaf: directory-entry hash (`docs/protocol.md`
523 /// §9.14, D4). Unlike the event log, the leaf hashes the entry ITSELF, not
524 /// its MAC: verifiers must see exactly what is proved.
525 pub const DIRECTORY_ENTRY: Label = Label::new(b"CC/v1/directory-entry");
526 /// Directory-log head signature. Separate from `audit-head`: a directory
527 /// head and an event-log head are different statements.
528 pub const DIRECTORY_HEAD: Label = Label::new(b"CC/v1/directory-head");
529 /// Semantic-mark layout fingerprint (D5): which points and which
530 /// equivalent variants they contain.
531 pub const MARK_LAYOUT: Label = Label::new(b"CC/v1/mark-layout");
532 /// Semantic-mark variant selection using the organization key (D5). Prefix-free with
533 /// `mark-layout`: they share only `"CC/v1/mark-"`.
534 pub const MARK_CHOICE: Label = Label::new(b"CC/v1/mark-choice");
535 /// Server recovery-package manifest signature using the lease-signing key
536 /// (E2, B5): which keys, state, and log head the package contains.
537 /// Does not affect the container.
538 pub const RECOVERY_MANIFEST: Label = Label::new(b"CC/v1/recovery-manifest");
539 /// Server-binding signature using the lease-signing key (E2, B2,
540 /// `oc_protocol::control::Binding`).
541 pub const AUTHORITY_BINDING: Label = Label::new(b"CC/v1/authority-binding");
542 /// Controller signature on an intent (E2, B2,
543 /// `oc_protocol::control::ControlRequest`).
544 pub const CONTROL_REQUEST: Label = Label::new(b"CC/v1/control-request");
545 /// Server signature on an operation receipt (E2, B2,
546 /// `oc_protocol::control::Receipt`). Prefix-free with `operation-id`: they share
547 /// only `"CC/v1/operation-"`.
548 pub const OPERATION_RECEIPT: Label = Label::new(b"CC/v1/operation-receipt");
549 /// Signature on a state snapshot sent to a replica (E2, B4,
550 /// `oc_protocol::replica::Push`).
551 pub const REPLICA_PUSH: Label = Label::new(b"CC/v1/replica-push");
552 /// Replica signature on an accepted snapshot (E2, B4,
553 /// `oc_protocol::replica::Ack`). Separate from `replica-push`: different
554 /// parties sign, and one's signature must not serve as the other's.
555 pub const REPLICA_ACK: Label = Label::new(b"CC/v1/replica-ack");
556 /// Controller signature transferring authority to a successor (E2, B7,
557 /// `oc_protocol::control::Transfer`). Separate from `control-request`:
558 /// an intent changes binding within an epoch; a transfer changes the epoch,
559 /// and a signature on one must not work for the other.
560 pub const AUTHORITY_TRANSFER: Label = Label::new(b"CC/v1/authority-transfer");
561 pub const CHUNK: Label = Label::new(b"CC/v1/chunk");
562 pub const LEAF: Label = Label::new(b"CC/v1/leaf");
563 pub const NODE: Label = Label::new(b"CC/v1/node");
564
565 pub const KEK: Label = Label::new(b"CC/v1/kek");
566 pub const PAYLOAD: Label = Label::new(b"CC/v1/payload");
567 pub const NONCE_BASE: Label = Label::new(b"CC/v1/nonce-base");
568 pub const PRIVATE_META: Label = Label::new(b"CC/v1/private-meta");
569 /// DEVICE keypair derived from a claim code.
570 ///
571 /// # Why a third code-related label was needed when two already existed
572 ///
573 /// `SLOT_B_CLAIM` derives share B directly from the code: that is how a
574 /// `RecipientClaim` slot works, correctly in that case: the recipient's share can be
575 /// anything, provided both parties derive the same value.
576 ///
577 /// For a code-based heir the share is FIXED: this file's share B, stored
578 /// in the author slot. It cannot be derived from an arbitrary code: derivation produces
579 /// what it produces. The code therefore derives a KEYPAIR, not a share, and the bequest
580 /// is sealed to its public key with ordinary `seal`, just as for any
581 /// device. No new primitives: the same HKDF, the same X25519, the
582 /// same `seal`.
583 ///
584 /// The label is separate and must remain so: using `SLOT_B_CLAIM` with the same
585 /// `ikm` would yield a private key equal to the slot share, so a code
586 /// opening one file would reveal the key used to sign another.
587 pub const CLAIM_DEVICE: Label = Label::new(b"CC/v1/claim-device");
588 pub const SLOT_B_CLAIM: Label = Label::new(b"CC/v1/slot-b-claim");
589 pub const SLOT_B_COMMIT: Label = Label::new(b"CC/v1/slot-b-commit");
590 pub const SLOT_COMMIT: Label = Label::new(b"CC/v1/slot-commit");
591 pub const A_TO_DEVICE: Label = Label::new(b"CC/v1/a-to-device");
592 /// Share B sent FROM THE AUTHOR to the recipient's device.
593 ///
594 /// A distinct domain from [`A_TO_DEVICE`], not symmetry for its own sake:
595 /// DIFFERENT parties issue shares under different decisions. If domains matched, a block
596 /// issued by the server could stand in for an author block, or vice versa.
597 ///
598 /// Prefix-free relative to `a-to-device` and all others (I-12): their initial
599 /// bytes differ.
600 pub const B_TO_DEVICE: Label = Label::new(b"CC/v1/b-to-device");
601 /// Deliberately not `"CC/v1/lease-cache"`: that string would extend
602 /// [`LEASE`], while labels also prefix HKDF `info`, with no
603 /// separating zero byte. The label set must be prefix-free.
604 pub const CACHED_LEASE: Label = Label::new(b"CC/v1/cached-lease");
605 pub const SEAL_KEY: Label = Label::new(b"CC/v1/seal-key");
606 /// Slot-sealing nonce hedging (§3.3).
607 ///
608 /// A **nonce** derivation label: its introduction clarifies rather than abandons
609 /// "nonces are stored, not derived". The reader still takes the nonce
610 /// **without computing it**, from the slot record. The sender derives it,
611 /// solely to stop the value being a pure function of
612 /// RNG state. See [`crate::kdf::hedged_nonce`].
613 pub const SEAL_NONCE: Label = Label::new(b"CC/v1/seal-nonce");
614 /// CEK-wrapper nonce hedging (§3.1). Same purpose as [`SEAL_NONCE`].
615 pub const WRAP_NONCE: Label = Label::new(b"CC/v1/wrap-nonce");
616 /// Payload-frame nonce hedging (§6.1).
617 ///
618 /// The fourth label serving this purpose; its later appearance was not about
619 /// completeness: decision C-13 was applied to slot sealing and CEK wrapping,
620 /// while two nonces, frame and private metadata, still took bytes directly
621 /// from the RNG. The same hole, simply in less conspicuous places.
622 ///
623 /// Deliberately **not** `"CC/v1/chunk-nonce"`: that string would extend the
624 /// [`CHUNK`] label, and labels also prefix HKDF `info`, where no
625 /// zero byte separates them: `"CC/v1/chunk"‖"-nonce"‖X` would equal
626 /// `"CC/v1/chunk-nonce"‖X`. Exactly the same case as [`CACHED_LEASE`], caught
627 /// by the same prefix-freeness test. Hence "frame" rather than "chunk":
628 /// the nonce belongs to the on-disk frame, not the logical chunk.
629 pub const FRAME_NONCE: Label = Label::new(b"CC/v1/frame-nonce");
630 /// Private-metadata nonce hedging (§2.0).
631 ///
632 /// Repeated RNG state cost more here than for a chunk: `CEK` and
633 /// `header_salt` come from the same RNG, so snapshot rollback
634 /// repeated both the K5 key and nonce while plaintexts (filename, size)
635 /// differed. This reuses the keystream and repeats the one-time
636 /// Poly1305 key inside the author-signed header.
637 pub const META_NONCE: Label = Label::new(b"CC/v1/meta-nonce");
638 /// Header-core hash: everything except key material.
639 pub const CORE_HASH: Label = Label::new(b"CC/v1/core-hash");
640 /// Policy hash over its byte range.
641 pub const POLICY_HASH: Label = Label::new(b"CC/v1/policy-hash");
642
643 /// Slot purpose: license-server share.
644 ///
645 /// Each slot kind has its own label in the sealing `info`.
646 /// Consequently, ciphertext addressed to the server does not open as ciphertext
647 /// addressed to the author's device, even if both are sealed to the same key.
648 pub const SLOT_SERVER: Label = Label::new(b"CC/v1/slot-server");
649 /// Slot purpose: recipient share.
650 pub const SLOT_RECIPIENT: Label = Label::new(b"CC/v1/slot-recipient");
651 /// Slot purpose: both shares for the author's device.
652 pub const SLOT_AUTHOR_DEVICE: Label = Label::new(b"CC/v1/slot-author-device");
653
654 /// Claim-code text → `claim_secret` (§3.4).
655 ///
656 /// Lives here although used in the client: the system has one label registry,
657 /// and declaring a label outside it breaks the only available prefix-freeness
658 /// guarantee, the test over [`ALL`]. There would be nothing to check it with,
659 /// precisely because the list is complete.
660 ///
661 /// The difference from [`SLOT_B_CLAIM`] matters: that label derives a **share** from
662 /// an existing 32-byte secret, while this one transforms
663 /// **printed text** into that secret. Different inputs, different domains.
664 pub const CLAIM_CODE: Label = Label::new(b"CC/v1/claim-code");
665 /// An author's wire instruction to the server to register or revoke a file.
666 /// Signed by the author key, the same one that signed the header.
667 pub const AUTHOR_ORDER: Label = Label::new(b"CC/v1/author-order");
668
669 /// Proof of opening a secret challenge (K23).
670 ///
671 /// **Unused by the protocol since 2026-09-21** (`docs/format.md`, section
672 /// "ECHO BOUND TO THE CONVERSATION 2026-09-21"): K31 derives the echo from
673 /// the handshake transcript. The label and derivation remain for the frozen
674 /// `k23_prove_echo` vector in `derivations_wire.kat`: I-14 forbids changing
675 /// frozen artifacts, and removing the label from the registry would remove
676 /// the tested domain beneath the vector.
677 pub const PROVE_ECHO: Label = Label::new(b"CC/v1/prove-echo");
678 /// Request MAC key after proof (K24).
679 pub const SESSION_MAC: Label = Label::new(b"CC/v1/session-mac");
680 /// Conversation-bound proof-of-possession echo (K31,
681 /// `docs/protocol.md` §9.4, decision 2026-09-21).
682 ///
683 /// One label for both steps of ONE derivation: it labels the handshake
684 /// transcript and also separates the HMAC domain whose key is the challenge
685 /// secret. There is no second domain, only "echo over transcript";
686 /// the label in the HMAC message prevents an echo colliding with K23 under
687 /// the same key.
688 ///
689 /// **The name deliberately does not extend `"CC/v1/prove-echo"`**: this set is
690 /// prefix-free, and `"CC/v1/prove-echo-bound"` would extend an already
691 /// occupied label, exactly what I-12 forbids. The nearest `e` neighbors,
692 /// `"CC/v1/editor-sig"`, `"CC/v1/editor-cert"`, `"CC/v1/edit-session"`,
693 /// and `"CC/v1/edition-claim"`, differ at byte eight (`c` versus
694 /// `d`).
695 pub const ECHO_TRANSCRIPT: Label = Label::new(b"CC/v1/echo-transcript");
696
697 /// Device fingerprint for mechanisms whose public keys exceed 32 bytes
698 /// (K27). For X25519 the fingerprint IS the key, and this label is unused:
699 /// that form is frozen by K11, K21, K23, and K24 vectors.
700 pub const DEVICE_FPR: Label = Label::new(b"CC/v1/device-fpr");
701
702 /// Wire operation identity (K28, `docs/protocol.md` §9.10).
703 ///
704 /// The identifier derives from a seed and the request body rather than coming directly
705 /// from the RNG, for the same reason as nonce hedging (C-13): RNGs
706 /// repeat after snapshot rollback and image cloning, and a server would treat two DIFFERENT
707 /// requests with one identifier as one request, giving the second
708 /// someone else's "issued" outcome.
709 pub const OPERATION_ID: Label = Label::new(b"CC/v1/operation-id");
710 /// Fresh value generated by the SERVER (K30, `docs/format.md`, section
711 /// "SERVER FRESHNESS IS DERIVED 2026-09-20").
712 ///
713 /// One label serves two purposes, attestation challenge (§9.11.1) and
714 /// proof-of-possession secret (§9.4), separated by `kind` inside the preimage,
715 /// as with [`OPERATION_ID`]. A second label would create a second domain where
716 /// there is only one: a server freshness value.
717 ///
718 /// Prefix-free: `"CC/v1/seal-key"`, `"CC/v1/seal-nonce"`, and
719 /// `"CC/v1/session-mac"` differ at byte eight, and no registry label
720 /// is its prefix.
721 pub const SERVER_FRESH: Label = Label::new(b"CC/v1/server-fresh");
722
723 /// Publisher-key signature on the DISTRIBUTION PACKAGE manifest (`manifest.txt` in a
724 /// Close Crate package, verified by `cc install`).
725 ///
726 /// # Why a label rather than "sign the manifest bytes"
727 ///
728 /// Because the publisher key is ordinary Ed25519, and without a domain its signature over
729 /// arbitrary bytes would work anywhere the same key verifies
730 /// something else. A distribution manifest has its own domain: it is neither a container,
731 /// protocol document, nor operator file, but an inventory of programs in an archive.
732 ///
733 /// The label does not affect the container at all: it changes no format version and enters no
734 /// header. It belongs in the registry not because the format needs it,
735 /// but because the registry is the ONLY place prefix-freeness is proved
736 /// (I-12): a label declared elsewhere is checked by nothing.
737 ///
738 /// Prefix-free: the nearest `p` labels are `"CC/v1/payload"`,
739 /// `"CC/v1/private-meta"`, `"CC/v1/policy-hash"`, and `"CC/v1/prove-echo"`;
740 /// all differ at byte eight; `"CC/v1/recovery-manifest"`
741 /// shares only a suffix, not a prefix.
742 pub const PACKAGE_MANIFEST: Label = Label::new(b"CC/v1/package-manifest");
743
744 /// K29: qualifying data for a TPM statement about the device key (B6a,
745 /// `docs/protocol.md` §9.11): `extraData` in `TPMS_ATTEST`.
746 ///
747 /// Separate from [`ATTEST_NONCE`]: that already serves as `info` when sealing a proof-of-possession
748 /// challenge (§9.4), and one label for two applications leaves
749 /// an unseparated domain (I-12). Prefix-free: `"CC/v1/attest-nonce"` is not
750 /// a prefix of this string, nor vice versa.
751 pub const ATTEST_QUALIFY: Label = Label::new(b"CC/v1/attest-qualify");
752
753 /// All labels in one list for the uniqueness test.
754 ///
755 /// The list remains the SOLE source for the four I-12 probes even after
756 /// [`Label`] was introduced: the type guards call sites, the list guards
757 /// the registry's contents. Neither check replaces the other.
758 pub const ALL: &[Label] = &[
759 HEADER_SIG, REVOCATION, GRANT, LEASE, ACTIVATE_REQ, AUDIT_ENTRY, AUDIT_HEAD,
760 ATTEST_NONCE,
761 CONTENT_MAC, EDITOR_SIG, CHUNK, LEAF, NODE, KEK, PAYLOAD, NONCE_BASE,
762 PRIVATE_META, CLAIM_DEVICE, SLOT_B_CLAIM, SLOT_B_COMMIT, SLOT_COMMIT, A_TO_DEVICE,
763 B_TO_DEVICE,
764 CACHED_LEASE,
765 SEAL_KEY, SEAL_NONCE, WRAP_NONCE, FRAME_NONCE, META_NONCE, CORE_HASH, POLICY_HASH,
766 SLOT_SERVER, SLOT_RECIPIENT, SLOT_AUTHOR_DEVICE, CLAIM_CODE, AUTHOR_ORDER, PROVE_ECHO, SESSION_MAC,
767 ECHO_TRANSCRIPT,
768 DEVICE_FPR, OPERATION_ID, ATTEST_QUALIFY, EDITOR_CERT, EDIT_SESSION, EDITION_CLAIM, FOOTER_IMPRINT,
769 WITNESS_COSIGN, DIRECTORY_ENTRY, DIRECTORY_HEAD, MARK_LAYOUT, MARK_CHOICE,
770 RECOVERY_MANIFEST, AUTHORITY_BINDING, CONTROL_REQUEST, OPERATION_RECEIPT,
771 REPLICA_PUSH, REPLICA_ACK, AUTHORITY_TRANSFER,
772 SERVER_FRESH, PACKAGE_MANIFEST,
773 AGENT_GRANT, DELEGATION,
774 ACTION_GRANT, ACTION_LEASE, ACTION_DECISION,
775 ];
776}
777
778/// Minimum claim-code entropy.
779///
780/// Not a cosmetic number. XChaCha20-Poly1305 is not key-committing, and without
781/// slot-commitment verification a low-entropy code can be recovered through
782/// a partitioning oracle substantially faster than exhaustive search. A six-digit code
783/// is unacceptable.
784///
785/// The boundary is checked **at build time**, not runtime, and could not be otherwise.
786/// `ClaimSecret::from_bytes` accepts any 32 bytes and must: by then
787/// the code has been hash-compressed and the result does not reveal entropy. It must be measured
788/// at generation, where it is exactly measurable: code length and alphabet size
789/// are known constants. The check lives in `cc_cli::claim` (`const _: () =
790/// assert!(...)`), so a short code will not "fail a test"; it will not compile.
791///
792/// Human-invented codes are absent from the product for the same reason: the commitment
793/// is plaintext in the container, guessing is offline, and attempt counts
794/// cannot be limited, since the guesses are not made against us.
795pub const MIN_CLAIM_BITS: u32 = 128;
796
797/// SHA-256 of a byte slice.
798///
799/// # Why a shared function when format hashes are computed in `oc-format`
800///
801/// Because not everything that needs hashing is format data. A distribution manifest
802/// lists programs and checksums; it is not a container, has no format versions,
803/// and creating a tag-registry entry for it would be a mistake.
804///
805/// It lives here rather than in `cc-cli` under the crate rules: `sha2` is already a dependency
806/// of this crate, and a second edge to `cc-cli` would introduce a direct
807/// dependency where none is needed. This crate remains pure:
808/// no I/O, clocks, or RNGs here.
809///
810/// Compare results only through [`digest_eq`].
811#[must_use]
812pub fn sha256(bytes: &[u8]) -> [u8; 32] {
813 use sha2::Digest as _;
814 sha2::Sha256::digest(bytes).into()
815}
816
817/// Compare two 32-byte digests in constant time.
818///
819/// The only way to compare hashes, roots, fingerprints, and commitments throughout the
820/// repository, a rule rather than an optimization. Some values are public
821/// (the tree root is plaintext in the file), some are not (the slot commitment), and
822/// the boundary moves over time: `original_root` is public today, but
823/// becomes an address in detached mode. Permitting "ordinary `==` here
824/// because the value is public" would oblige us to prove that again with
825/// every change, and eventually prove it incorrectly.
826///
827/// Comparison accepts fixed-length arrays rather than slices: a length that can
828/// be confused is a second way to err, which is unnecessary here.
829#[must_use]
830pub fn digest_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
831 use subtle::ConstantTimeEq;
832 bool::from(a.ct_eq(b))
833}
834
835/// Compare two PUBLIC keys of possibly different lengths in constant time.
836///
837/// A separate function, not [`digest_eq`] weakened to slices; this distinction is essential.
838/// The rule "comparison accepts fixed-length arrays" prevents
839/// length from becoming a second source of errors; extending the exception to every
840/// comparison in the repository would abolish that rule. Exactly one circumstance
841/// justifies this exception: key length is a function of the mechanism (`kem_id`), differing for
842/// X25519 and P-256, and both comparison operands come from the **author-signed**
843/// header, where length is public by construction.
844///
845/// Length mismatch returns `false` immediately, before comparing contents, without leaking
846/// anything: the length is a public number in the file, already known to an attacker. This means
847/// "the slot is not for our mechanism", hence "not our slot", exactly the same as
848/// mismatching key bytes.
849///
850/// Contents are compared using `subtle` (I-13): the key is public, but response timing
851/// must not reveal how many bytes matched, or a byte-by-byte oracle for guessing
852/// the slot's recipient would emerge.
853#[must_use]
854pub fn public_key_eq(a: &[u8], b: &[u8]) -> bool {
855 use subtle::ConstantTimeEq;
856 if a.len() != b.len() {
857 return false;
858 }
859 bool::from(a.ct_eq(b))
860}
861
862#[cfg(test)]
863#[allow(clippy::unwrap_used, clippy::panic)]
864mod tests {
865 use super::*;
866 use std::collections::BTreeSet;
867
868 #[test]
869 fn digest_comparison_agrees_with_equality_on_every_byte_position() {
870 // Проверяется не «работает ли ct_eq» — за это отвечает subtle, — а то, что
871 // единый способ сравнения не разошёлся с обычным равенством ни в одном
872 // разряде. Ошибка вида «сравнили первые 16 байт» прошла бы мимо теста на
873 // паре случайных значений, но не мимо перебора позиций.
874 let base = [0xA5u8; 32];
875 assert!(digest_eq(&base, &base.clone()));
876 for position in 0..32usize {
877 let mut other = base;
878 if let Some(byte) = other.get_mut(position) {
879 *byte ^= 0x80;
880 }
881 assert!(!digest_eq(&base, &other), "различие в байте {position} не замечено");
882 }
883 }
884
885 #[test]
886 fn every_domain_label_is_unique() {
887 // Повторно использованная метка — тихая уязвимость: подпись из одного
888 // контекста начинает приниматься в другом. Дешевле поймать тестом.
889 // Сравниваются БАЙТЫ, а не значения `Label`: две константы с одной и той
890 // же строкой — это и есть повтор домена, и `Label` их не различает лишь
891 // потому, что различать там нечего. Сверка по байтам оставляет проверку
892 // той же, какой она была до появления типа.
893 let unique: BTreeSet<&[u8]> = label::ALL.iter().map(|l| l.as_bytes()).collect();
894 assert_eq!(unique.len(), label::ALL.len(), "метки домена повторяются");
895 }
896
897 #[test]
898 fn no_label_is_a_prefix_of_another() {
899 // Префиксная метка позволяет столкнуть кодировки: "CC/v1/lease" и
900 // "CC/v1/lease-cache" различаются только тем, что идёт дальше.
901 // Разделитель 0x00 в транскрипте закрывает это, тест фиксирует намерение.
902 for a in label::ALL.iter().map(|l| l.as_bytes()) {
903 for b in label::ALL.iter().map(|l| l.as_bytes()) {
904 if a != b {
905 assert!(!b.starts_with(a) || b.len() == a.len(), "метка {a:?} — префикс {b:?}");
906 }
907 }
908 }
909 }
910
911 #[test]
912 fn every_domain_label_is_versioned() {
913 for l in label::ALL.iter().map(|l| l.as_bytes()) {
914 assert!(
915 l.starts_with(b"CC/v1/"),
916 "метка {:?} без версии: при переходе на v2 её нельзя будет отличить",
917 core::str::from_utf8(l).unwrap_or("<не utf8>")
918 );
919 }
920 }
921
922 #[test]
923 fn algorithm_ids_are_stable_numbers() {
924 // Значения входят в подписанный транскрипт, поэтому их нельзя менять
925 // местами при рефакторинге: старые файлы перестанут проверяться.
926 assert_eq!(AeadAlg::XChaCha20Poly1305 as u8, 1);
927 assert_eq!(SigAlg::Ed25519 as u8, 1);
928 assert_eq!(KemAlg::X25519HkdfSha256 as u8, 1);
929 assert_eq!(KemAlg::P256HkdfSha256 as u8, 2);
930 assert_eq!(KemAlg::XWing as u8, 4);
931 assert_eq!(KemAlg::MlKem768P256 as u8, 5);
932 assert_eq!(TreeHashAlg::Blake3 as u8, 1);
933 }
934
935 #[test]
936 fn unknown_algorithm_ids_are_refused_not_defaulted() {
937 for v in [0u8, 6, 99, 255] {
938 assert_eq!(AeadAlg::from_u8(v), Err(CryptoError::UnsupportedAlgorithm));
939 assert!(KemAlg::from_u8(v).is_err(), "неизвестный kem_id {v} принят");
940 }
941 // ЧЕТВЁРКА ОТСЮДА УБРАНА, и это тот же случай, что с `SigAlg::from_u8(2)`
942 // ниже. Она перестала быть неизвестным номером: её занял гибрид X-Wing
943 // решением версии 4. Оставь её здесь — и проба утверждала бы, что формат
944 // четвёртого механизма не знает, ровно тогда, когда он заработал.
945 assert!(KemAlg::from_u8(4).is_ok(), "четвёрка занята гибридом X-Wing");
946 // Пятёрка ушла отсюда по той же причине, что и четвёрка до неё: её занял
947 // аппаратный гибрид MLKEM768-P256 решением версии 5. Список неизвестных
948 // номеров тает с каждым занятым, и это нормально — он про НЕЗАНЯТЫЕ.
949 assert!(KemAlg::from_u8(5).is_ok(), "пятёрка занята аппаратным гибридом");
950 // `SigAlg::from_u8(2)` ЗДЕСЬ БОЛЬШЕ НЕ ПРОВЕРЯЕТСЯ, и это не упущение.
951 // Двойка перестала быть неизвестным номером: она занята RSA-PSS решением
952 // версии 3. Отказ остался тем же, но означает другое — «занято, не
953 // исполняется», — и проба под именем «неизвестные номера» утверждала бы
954 // неправду. Перенесено ниже, к своим соседям.
955 assert_eq!(SigAlg::from_u8(3), Err(CryptoError::UnsupportedAlgorithm));
956 }
957
958 #[test]
959 fn both_signature_algorithms_are_executable_and_numbered_stably() {
960 // Обе схемы сборка теперь исполняет: Ed25519 подписывает автор, RSA-PSS
961 // — редактировавшее устройство. Номера входят в подписанный транскрипт
962 // (`verify::suite_id`), поэтому меняться местами не вправе.
963 //
964 // «Умеем исполнить» — не то же, что «годится здесь»: `suite.sig_alg`
965 // допускает только Ed25519, и эту проверку ставит разбор заголовка. Она
966 // проверяется там же, у своего места, а не здесь.
967 assert_eq!(SigAlg::Ed25519 as u8, 1);
968 assert_eq!(SigAlg::RsaPssSha256 as u8, 2);
969 assert_eq!(SigAlg::from_u8(1), Ok(SigAlg::Ed25519));
970 assert_eq!(SigAlg::from_u8(2), Ok(SigAlg::RsaPssSha256));
971 assert_eq!(SigAlg::Ed25519.ensure_supported(), Ok(()));
972 assert_eq!(SigAlg::RsaPssSha256.ensure_supported(), Ok(()));
973 }
974
975 #[test]
976 fn an_aead_id_this_build_cannot_execute_is_refused_at_parse_time() {
977 // Симметрично TreeHashAlg. Номера 2 и 3 в реестре формата существуют
978 // (docs/format.md §6.1), но профилей AES в сборке нет, поэтому отказ
979 // обязан приходить на разборе, а не на первом чанке — иначе подпись,
980 // слот, согласование ключей и разворот CEK делаются впустую над файлом,
981 // про который уже всё известно.
982 for v in [2u8, 3] {
983 assert_eq!(
984 AeadAlg::from_u8(v),
985 Err(CryptoError::UnsupportedAlgorithm),
986 "aead_id {v} принят разбором, хотя исполнять его нечем"
987 );
988 }
989 assert_eq!(AeadAlg::from_u8(1), Ok(AeadAlg::XChaCha20Poly1305));
990 }
991}