pub mod aead;
pub mod agreement;
pub mod kdf;
pub mod mac;
pub mod merkle;
pub mod mlkem_p256;
pub mod rsa;
#[cfg(any(test, feature = "test-signer"))]
pub mod rsa_test_signer;
pub mod seal;
pub mod secret;
pub mod sign;
pub mod stream;
pub mod tpm;
pub mod transcript;
pub mod wrap;
pub mod xwing;
pub use label::Label;
pub use secret::{
Cek, ClaimSecret, Kek, MacKey, MetaKey, PayloadKey, SecretA, SecretB, SecretBuf, X25519Secret,
};
pub use transcript::Transcript;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoError {
Authentication,
BadSignature,
BadLength,
TreeMismatch,
IndexOutOfRange,
BadKey,
UnsupportedAlgorithm,
}
impl core::fmt::Display for CryptoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let text = match self {
Self::Authentication => "проверка подлинности не прошла",
Self::BadSignature => "подпись неверна",
Self::BadLength => "некорректная длина данных",
Self::TreeMismatch => "данные не соответствуют дереву целостности",
Self::IndexOutOfRange => "индекс за пределами дерева",
Self::BadKey => "некорректный ключ",
Self::UnsupportedAlgorithm => "алгоритм не поддерживается",
};
f.write_str(text)
}
}
impl core::error::Error for CryptoError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AeadAlg {
XChaCha20Poly1305 = 1,
Aes256Gcm = 2,
Aes256GcmSiv = 3,
}
impl AeadAlg {
pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
let alg = match v {
1 => Self::XChaCha20Poly1305,
2 => Self::Aes256Gcm,
3 => Self::Aes256GcmSiv,
_ => return Err(CryptoError::UnsupportedAlgorithm),
};
aead::ensure_supported(alg)?;
Ok(alg)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SigAlg {
Ed25519 = 1,
RsaPssSha256 = 2,
}
impl SigAlg {
pub fn ensure_supported(self) -> Result<(), CryptoError> {
match self {
Self::Ed25519 => Ok(()),
Self::RsaPssSha256 => Ok(()),
}
}
pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
let alg = match v {
1 => Self::Ed25519,
2 => Self::RsaPssSha256,
_ => return Err(CryptoError::UnsupportedAlgorithm),
};
alg.ensure_supported()?;
Ok(alg)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum KemAlg {
X25519HkdfSha256 = 1,
P256HkdfSha256 = 2,
RsaOaepSha256 = 3,
XWing = 4,
MlKem768P256 = 5,
}
impl KemAlg {
pub fn ensure_supported(self) -> Result<(), CryptoError> {
if seal::supports_kem(self) { Ok(()) } else { Err(CryptoError::UnsupportedAlgorithm) }
}
pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
match v {
1 => Ok(Self::X25519HkdfSha256),
2 => Ok(Self::P256HkdfSha256),
3 => Ok(Self::RsaOaepSha256),
4 => Ok(Self::XWing),
5 => Ok(Self::MlKem768P256),
_ => Err(CryptoError::UnsupportedAlgorithm),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TreeHashAlg {
Blake3 = 1,
Sha256 = 2,
}
impl TreeHashAlg {
pub fn from_u8(v: u8) -> Result<Self, CryptoError> {
let alg = match v {
1 => Self::Blake3,
2 => Self::Sha256,
_ => return Err(CryptoError::UnsupportedAlgorithm),
};
merkle::ensure_supported(alg)?;
Ok(alg)
}
}
pub mod label {
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Label(&'static [u8]);
impl Label {
const fn new(bytes: &'static [u8]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(self) -> &'static [u8] {
self.0
}
#[must_use]
pub const fn len(self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0.is_empty()
}
#[cfg(any(test, feature = "ad-hoc-label"))]
#[must_use]
pub const fn ad_hoc(bytes: &'static [u8]) -> Self {
Self::new(bytes)
}
}
impl core::fmt::Debug for Label {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match core::str::from_utf8(self.0) {
Ok(text) => write!(f, "Label({text})"),
Err(_) => write!(f, "Label({:?})", self.0),
}
}
}
pub const HEADER_SIG: Label = Label::new(b"CC/v1/header-sig");
pub const REVOCATION: Label = Label::new(b"CC/v1/revocation");
pub const GRANT: Label = Label::new(b"CC/v1/grant");
pub const AGENT_GRANT: Label = Label::new(b"CC/v1/agent-grant");
pub const DELEGATION: Label = Label::new(b"CC/v1/delegation");
pub const ACTION_GRANT: Label = Label::new(b"CC/v1/action-grant");
pub const ACTION_LEASE: Label = Label::new(b"CC/v1/action-lease");
pub const ACTION_DECISION: Label = Label::new(b"CC/v1/action-decision");
pub const LEASE: Label = Label::new(b"CC/v1/lease");
pub const ACTIVATE_REQ: Label = Label::new(b"CC/v1/activate-req");
pub const AUDIT_ENTRY: Label = Label::new(b"CC/v1/audit-entry");
pub const AUDIT_HEAD: Label = Label::new(b"CC/v1/audit-head");
pub const ATTEST_NONCE: Label = Label::new(b"CC/v1/attest-nonce");
pub const CONTENT_MAC: Label = Label::new(b"CC/v1/content-mac");
pub const EDITOR_SIG: Label = Label::new(b"CC/v1/editor-sig");
pub const EDITOR_CERT: Label = Label::new(b"CC/v1/editor-cert");
pub const EDIT_SESSION: Label = Label::new(b"CC/v1/edit-session");
pub const EDITION_CLAIM: Label = Label::new(b"CC/v1/edition-claim");
pub const FOOTER_IMPRINT: Label = Label::new(b"CC/v1/footer-imprint");
pub const WITNESS_COSIGN: Label = Label::new(b"CC/v1/witness-cosign");
pub const DIRECTORY_ENTRY: Label = Label::new(b"CC/v1/directory-entry");
pub const DIRECTORY_HEAD: Label = Label::new(b"CC/v1/directory-head");
pub const MARK_LAYOUT: Label = Label::new(b"CC/v1/mark-layout");
pub const MARK_CHOICE: Label = Label::new(b"CC/v1/mark-choice");
pub const RECOVERY_MANIFEST: Label = Label::new(b"CC/v1/recovery-manifest");
pub const AUTHORITY_BINDING: Label = Label::new(b"CC/v1/authority-binding");
pub const CONTROL_REQUEST: Label = Label::new(b"CC/v1/control-request");
pub const OPERATION_RECEIPT: Label = Label::new(b"CC/v1/operation-receipt");
pub const REPLICA_PUSH: Label = Label::new(b"CC/v1/replica-push");
pub const REPLICA_ACK: Label = Label::new(b"CC/v1/replica-ack");
pub const AUTHORITY_TRANSFER: Label = Label::new(b"CC/v1/authority-transfer");
pub const CHUNK: Label = Label::new(b"CC/v1/chunk");
pub const LEAF: Label = Label::new(b"CC/v1/leaf");
pub const NODE: Label = Label::new(b"CC/v1/node");
pub const KEK: Label = Label::new(b"CC/v1/kek");
pub const PAYLOAD: Label = Label::new(b"CC/v1/payload");
pub const NONCE_BASE: Label = Label::new(b"CC/v1/nonce-base");
pub const PRIVATE_META: Label = Label::new(b"CC/v1/private-meta");
pub const CLAIM_DEVICE: Label = Label::new(b"CC/v1/claim-device");
pub const SLOT_B_CLAIM: Label = Label::new(b"CC/v1/slot-b-claim");
pub const SLOT_B_COMMIT: Label = Label::new(b"CC/v1/slot-b-commit");
pub const SLOT_COMMIT: Label = Label::new(b"CC/v1/slot-commit");
pub const A_TO_DEVICE: Label = Label::new(b"CC/v1/a-to-device");
pub const B_TO_DEVICE: Label = Label::new(b"CC/v1/b-to-device");
pub const CACHED_LEASE: Label = Label::new(b"CC/v1/cached-lease");
pub const SEAL_KEY: Label = Label::new(b"CC/v1/seal-key");
pub const SEAL_NONCE: Label = Label::new(b"CC/v1/seal-nonce");
pub const WRAP_NONCE: Label = Label::new(b"CC/v1/wrap-nonce");
pub const FRAME_NONCE: Label = Label::new(b"CC/v1/frame-nonce");
pub const META_NONCE: Label = Label::new(b"CC/v1/meta-nonce");
pub const CORE_HASH: Label = Label::new(b"CC/v1/core-hash");
pub const POLICY_HASH: Label = Label::new(b"CC/v1/policy-hash");
pub const SLOT_SERVER: Label = Label::new(b"CC/v1/slot-server");
pub const SLOT_RECIPIENT: Label = Label::new(b"CC/v1/slot-recipient");
pub const SLOT_AUTHOR_DEVICE: Label = Label::new(b"CC/v1/slot-author-device");
pub const CLAIM_CODE: Label = Label::new(b"CC/v1/claim-code");
pub const AUTHOR_ORDER: Label = Label::new(b"CC/v1/author-order");
pub const PROVE_ECHO: Label = Label::new(b"CC/v1/prove-echo");
pub const SESSION_MAC: Label = Label::new(b"CC/v1/session-mac");
pub const ECHO_TRANSCRIPT: Label = Label::new(b"CC/v1/echo-transcript");
pub const DEVICE_FPR: Label = Label::new(b"CC/v1/device-fpr");
pub const OPERATION_ID: Label = Label::new(b"CC/v1/operation-id");
pub const SERVER_FRESH: Label = Label::new(b"CC/v1/server-fresh");
pub const PACKAGE_MANIFEST: Label = Label::new(b"CC/v1/package-manifest");
pub const ATTEST_QUALIFY: Label = Label::new(b"CC/v1/attest-qualify");
pub const ALL: &[Label] = &[
HEADER_SIG, REVOCATION, GRANT, LEASE, ACTIVATE_REQ, AUDIT_ENTRY, AUDIT_HEAD,
ATTEST_NONCE,
CONTENT_MAC, EDITOR_SIG, CHUNK, LEAF, NODE, KEK, PAYLOAD, NONCE_BASE,
PRIVATE_META, CLAIM_DEVICE, SLOT_B_CLAIM, SLOT_B_COMMIT, SLOT_COMMIT, A_TO_DEVICE,
B_TO_DEVICE,
CACHED_LEASE,
SEAL_KEY, SEAL_NONCE, WRAP_NONCE, FRAME_NONCE, META_NONCE, CORE_HASH, POLICY_HASH,
SLOT_SERVER, SLOT_RECIPIENT, SLOT_AUTHOR_DEVICE, CLAIM_CODE, AUTHOR_ORDER, PROVE_ECHO, SESSION_MAC,
ECHO_TRANSCRIPT,
DEVICE_FPR, OPERATION_ID, ATTEST_QUALIFY, EDITOR_CERT, EDIT_SESSION, EDITION_CLAIM, FOOTER_IMPRINT,
WITNESS_COSIGN, DIRECTORY_ENTRY, DIRECTORY_HEAD, MARK_LAYOUT, MARK_CHOICE,
RECOVERY_MANIFEST, AUTHORITY_BINDING, CONTROL_REQUEST, OPERATION_RECEIPT,
REPLICA_PUSH, REPLICA_ACK, AUTHORITY_TRANSFER,
SERVER_FRESH, PACKAGE_MANIFEST,
AGENT_GRANT, DELEGATION,
ACTION_GRANT, ACTION_LEASE, ACTION_DECISION,
];
}
pub const MIN_CLAIM_BITS: u32 = 128;
#[must_use]
pub fn sha256(bytes: &[u8]) -> [u8; 32] {
use sha2::Digest as _;
sha2::Sha256::digest(bytes).into()
}
#[must_use]
pub fn digest_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
use subtle::ConstantTimeEq;
bool::from(a.ct_eq(b))
}
#[must_use]
pub fn public_key_eq(a: &[u8], b: &[u8]) -> bool {
use subtle::ConstantTimeEq;
if a.len() != b.len() {
return false;
}
bool::from(a.ct_eq(b))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn digest_comparison_agrees_with_equality_on_every_byte_position() {
let base = [0xA5u8; 32];
assert!(digest_eq(&base, &base.clone()));
for position in 0..32usize {
let mut other = base;
if let Some(byte) = other.get_mut(position) {
*byte ^= 0x80;
}
assert!(!digest_eq(&base, &other), "различие в байте {position} не замечено");
}
}
#[test]
fn every_domain_label_is_unique() {
let unique: BTreeSet<&[u8]> = label::ALL.iter().map(|l| l.as_bytes()).collect();
assert_eq!(unique.len(), label::ALL.len(), "метки домена повторяются");
}
#[test]
fn no_label_is_a_prefix_of_another() {
for a in label::ALL.iter().map(|l| l.as_bytes()) {
for b in label::ALL.iter().map(|l| l.as_bytes()) {
if a != b {
assert!(!b.starts_with(a) || b.len() == a.len(), "метка {a:?} — префикс {b:?}");
}
}
}
}
#[test]
fn every_domain_label_is_versioned() {
for l in label::ALL.iter().map(|l| l.as_bytes()) {
assert!(
l.starts_with(b"CC/v1/"),
"метка {:?} без версии: при переходе на v2 её нельзя будет отличить",
core::str::from_utf8(l).unwrap_or("<не utf8>")
);
}
}
#[test]
fn algorithm_ids_are_stable_numbers() {
assert_eq!(AeadAlg::XChaCha20Poly1305 as u8, 1);
assert_eq!(SigAlg::Ed25519 as u8, 1);
assert_eq!(KemAlg::X25519HkdfSha256 as u8, 1);
assert_eq!(KemAlg::P256HkdfSha256 as u8, 2);
assert_eq!(KemAlg::XWing as u8, 4);
assert_eq!(KemAlg::MlKem768P256 as u8, 5);
assert_eq!(TreeHashAlg::Blake3 as u8, 1);
}
#[test]
fn unknown_algorithm_ids_are_refused_not_defaulted() {
for v in [0u8, 6, 99, 255] {
assert_eq!(AeadAlg::from_u8(v), Err(CryptoError::UnsupportedAlgorithm));
assert!(KemAlg::from_u8(v).is_err(), "неизвестный kem_id {v} принят");
}
assert!(KemAlg::from_u8(4).is_ok(), "четвёрка занята гибридом X-Wing");
assert!(KemAlg::from_u8(5).is_ok(), "пятёрка занята аппаратным гибридом");
assert_eq!(SigAlg::from_u8(3), Err(CryptoError::UnsupportedAlgorithm));
}
#[test]
fn both_signature_algorithms_are_executable_and_numbered_stably() {
assert_eq!(SigAlg::Ed25519 as u8, 1);
assert_eq!(SigAlg::RsaPssSha256 as u8, 2);
assert_eq!(SigAlg::from_u8(1), Ok(SigAlg::Ed25519));
assert_eq!(SigAlg::from_u8(2), Ok(SigAlg::RsaPssSha256));
assert_eq!(SigAlg::Ed25519.ensure_supported(), Ok(()));
assert_eq!(SigAlg::RsaPssSha256.ensure_supported(), Ok(()));
}
#[test]
fn an_aead_id_this_build_cannot_execute_is_refused_at_parse_time() {
for v in [2u8, 3] {
assert_eq!(
AeadAlg::from_u8(v),
Err(CryptoError::UnsupportedAlgorithm),
"aead_id {v} принят разбором, хотя исполнять его нечем"
);
}
assert_eq!(AeadAlg::from_u8(1), Ok(AeadAlg::XChaCha20Poly1305));
}
}