iroh_db_security/
commit.rs

1use chacha20poly1305::{
2    XChaCha20Poly1305, XNonce,
3    aead::{Aead, KeyInit, Payload},
4};
5use hkdf::Hkdf;
6use iroh_db_core::{CommitBody, CommitCodecError, CommitEnvelope, CommitHeader, CommitSignature};
7use sha2::Sha256;
8use zeroize::Zeroizing;
9
10use crate::{DomainKey, IrohSigner, key::commit_signature_message};
11
12/// Cryptographic validation or commit protection failure.
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum SecurityError {
15    /// The header author did not match the supplied endpoint signer.
16    #[error("commit author does not match the endpoint signer")]
17    AuthorMismatch,
18    /// The encoded endpoint public key was invalid.
19    #[error("commit author is not a valid iroh endpoint id")]
20    InvalidAuthor,
21    /// The commit signature was invalid.
22    #[error("commit signature validation failed")]
23    InvalidSignature,
24    /// Authenticated encryption could not be completed.
25    #[error("commit encryption failed")]
26    EncryptionFailed,
27    /// Ciphertext authentication or decryption failed.
28    #[error("commit decryption failed")]
29    DecryptionFailed,
30    /// The commit sub-key could not be derived.
31    #[error("commit key derivation failed")]
32    KeyDerivationFailed,
33    /// The operating system random source was unavailable.
34    #[error("cryptographic randomness is unavailable")]
35    RandomnessUnavailable,
36    /// Local key-provider I/O failed without exposing key material.
37    #[error("local key provider I/O failed: {0}")]
38    KeyProviderIo(String),
39    /// Local key-provider bytes failed format or authentication checks.
40    #[error("local key provider data is invalid")]
41    InvalidKeyProviderData,
42    /// Secret key files were accessible by group or other users.
43    #[error("local key provider file permissions are not owner-only")]
44    InsecureKeyPermissions,
45    /// The canonical commit representation was invalid.
46    #[error(transparent)]
47    CommitCodec(#[from] CommitCodecError),
48}
49
50/// Encrypts and signs one canonical atomic commit.
51pub fn seal_commit(
52    header: CommitHeader,
53    body: &CommitBody,
54    domain_key: &DomainKey,
55    signer: &IrohSigner,
56) -> Result<CommitEnvelope, SecurityError> {
57    if header.author() != signer.author() {
58        return Err(SecurityError::AuthorMismatch);
59    }
60
61    let key = derive_commit_key(domain_key, &header)?;
62    let cipher = XChaCha20Poly1305::new((&*key).into());
63    let associated_data = CommitEnvelope::signing_bytes_for(&header, &[])?;
64    let nonce = XNonce::from(*header.nonce());
65    let ciphertext = cipher
66        .encrypt(
67            &nonce,
68            Payload {
69                msg: &body.encode_canonical(),
70                aad: &associated_data,
71            },
72        )
73        .map_err(|_| SecurityError::EncryptionFailed)?;
74    let signing_bytes = CommitEnvelope::signing_bytes_for(&header, &ciphertext)?;
75    let signature = CommitSignature::from_bytes(signer.sign(&signing_bytes));
76    Ok(CommitEnvelope::new(header, ciphertext, signature)?)
77}
78
79/// Verifies, authenticates, decrypts, and strictly decodes a commit.
80pub fn open_commit(
81    envelope: &CommitEnvelope,
82    domain_key: &DomainKey,
83) -> Result<CommitBody, SecurityError> {
84    verify_signature(envelope)?;
85
86    let key = derive_commit_key(domain_key, envelope.header())?;
87    let cipher = XChaCha20Poly1305::new((&*key).into());
88    let associated_data = CommitEnvelope::signing_bytes_for(envelope.header(), &[])?;
89    let nonce = XNonce::from(*envelope.header().nonce());
90    let plaintext = cipher
91        .decrypt(
92            &nonce,
93            Payload {
94                msg: envelope.ciphertext(),
95                aad: &associated_data,
96            },
97        )
98        .map_err(|_| SecurityError::DecryptionFailed)?;
99    Ok(CommitBody::decode_canonical(&plaintext)?)
100}
101
102fn verify_signature(envelope: &CommitEnvelope) -> Result<(), SecurityError> {
103    let public_key = iroh_base::PublicKey::try_from(envelope.header().author().as_bytes())
104        .map_err(|_| SecurityError::InvalidAuthor)?;
105    let signature = iroh_base::Signature::from_bytes(envelope.signature().as_bytes());
106    public_key
107        .verify(
108            &commit_signature_message(&envelope.signing_bytes()),
109            &signature,
110        )
111        .map_err(|_| SecurityError::InvalidSignature)
112}
113
114fn derive_commit_key(
115    domain_key: &DomainKey,
116    header: &CommitHeader,
117) -> Result<Zeroizing<[u8; 32]>, SecurityError> {
118    let hkdf = Hkdf::<Sha256>::new(Some(header.domain_id().as_bytes()), domain_key.as_bytes());
119    let mut info = Vec::with_capacity(25 + size_of::<u64>());
120    info.extend_from_slice(b"iroh-db/commit-key/v1");
121    info.extend_from_slice(&header.epoch().to_be_bytes());
122    let mut output = Zeroizing::new([0_u8; 32]);
123    hkdf.expand(&info, &mut *output)
124        .map_err(|_| SecurityError::KeyDerivationFailed)?;
125    Ok(output)
126}