iroh_db_security/
commit.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum SecurityError {
15 #[error("commit author does not match the endpoint signer")]
17 AuthorMismatch,
18 #[error("commit author is not a valid iroh endpoint id")]
20 InvalidAuthor,
21 #[error("commit signature validation failed")]
23 InvalidSignature,
24 #[error("commit encryption failed")]
26 EncryptionFailed,
27 #[error("commit decryption failed")]
29 DecryptionFailed,
30 #[error("commit key derivation failed")]
32 KeyDerivationFailed,
33 #[error("cryptographic randomness is unavailable")]
35 RandomnessUnavailable,
36 #[error("local key provider I/O failed: {0}")]
38 KeyProviderIo(String),
39 #[error("local key provider data is invalid")]
41 InvalidKeyProviderData,
42 #[error("local key provider file permissions are not owner-only")]
44 InsecureKeyPermissions,
45 #[error(transparent)]
47 CommitCodec(#[from] CommitCodecError),
48}
49
50pub 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
79pub 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}