Feature 1 — Post-Quantum Hybrid KEM (X25519 + ML-KEM-768): - Create hybrid_kem.rs with keygen, encrypt, decrypt + 11 unit tests - Wire format: version(1) | x25519_eph_pk(32) | mlkem_ct(1088) | nonce(12) | ct - Add uploadHybridKey/fetchHybridKey RPCs to node.capnp schema - Server: hybrid key storage in FileBackedStore + RPC handlers - Client: hybrid keypair in StoredState, auto-wrap/unwrap in send/recv/invite/join - demo-group runs full hybrid PQ envelope round-trip Feature 2 — SQLCipher Persistence: - Extract Store trait from FileBackedStore API - Create SqlStore (rusqlite + bundled-sqlcipher) with encrypted-at-rest SQLite - Schema: key_packages, deliveries, hybrid_keys tables with indexes - Server CLI: --store-backend=sql, --db-path, --db-key flags - 5 unit tests for SqlStore (FIFO, round-trip, upsert, channel isolation) Also includes: client lib.rs refactor, auth config, TOML config file support, mdBook documentation, and various cleanups by user. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.4 KiB
Rust
136 lines
4.4 KiB
Rust
//! Ed25519 identity keypair for MLS credentials and AS registration.
|
|
//!
|
|
//! The [`IdentityKeypair`] is the long-term identity key embedded in MLS
|
|
//! `BasicCredential`s. It is used for signing MLS messages and as the
|
|
//! indexing key for the Authentication Service.
|
|
//!
|
|
//! # Zeroize
|
|
//!
|
|
//! The 32-byte private seed is stored as `Zeroizing<[u8; 32]>`, which zeroes
|
|
//! the bytes on drop. `[u8; 32]` is `Copy + Default` and satisfies zeroize's
|
|
//! `DefaultIsZeroes` constraint, avoiding a conflict with ed25519-dalek's
|
|
//! `SigningKey` zeroize impl.
|
|
//!
|
|
//! # Fingerprint
|
|
//!
|
|
//! A 32-byte SHA-256 digest of the raw public key bytes is used as a compact,
|
|
//! collision-resistant identifier for logging.
|
|
|
|
use ed25519_dalek::{Signer as DalekSigner, SigningKey, VerifyingKey};
|
|
use openmls_traits::signatures::Signer;
|
|
use openmls_traits::types::{Error as MlsError, SignatureScheme};
|
|
use serde::{Deserialize, Serialize};
|
|
use sha2::{Digest, Sha256};
|
|
use zeroize::Zeroizing;
|
|
|
|
/// An Ed25519 identity keypair.
|
|
///
|
|
/// Created with [`IdentityKeypair::generate`]. The private signing key seed
|
|
/// is zeroed when this struct is dropped.
|
|
pub struct IdentityKeypair {
|
|
/// Raw 32-byte private seed — zeroized on drop.
|
|
///
|
|
/// Stored as bytes rather than `SigningKey` to satisfy zeroize's
|
|
/// `DefaultIsZeroes` bound on `Zeroizing<T>`.
|
|
seed: Zeroizing<[u8; 32]>,
|
|
/// Corresponding 32-byte public verifying key.
|
|
verifying: VerifyingKey,
|
|
}
|
|
|
|
impl IdentityKeypair {
|
|
/// Recreate an identity keypair from a 32-byte seed.
|
|
pub fn from_seed(seed: [u8; 32]) -> Self {
|
|
let signing = SigningKey::from_bytes(&seed);
|
|
let verifying = signing.verifying_key();
|
|
Self {
|
|
seed: Zeroizing::new(seed),
|
|
verifying,
|
|
}
|
|
}
|
|
|
|
/// Return the raw 32-byte private seed (for persistence).
|
|
pub fn seed_bytes(&self) -> [u8; 32] {
|
|
*self.seed
|
|
}
|
|
}
|
|
|
|
impl IdentityKeypair {
|
|
/// Generate a fresh random Ed25519 identity keypair.
|
|
pub fn generate() -> Self {
|
|
use rand::rngs::OsRng;
|
|
let signing = SigningKey::generate(&mut OsRng);
|
|
let verifying = signing.verifying_key();
|
|
let seed = Zeroizing::new(signing.to_bytes());
|
|
Self { seed, verifying }
|
|
}
|
|
|
|
/// Return the raw 32-byte Ed25519 public key.
|
|
///
|
|
/// This is the byte array used as `identityKey` in `auth.capnp` calls.
|
|
pub fn public_key_bytes(&self) -> [u8; 32] {
|
|
self.verifying.to_bytes()
|
|
}
|
|
|
|
/// Return the SHA-256 fingerprint of the public key (32 bytes).
|
|
pub fn fingerprint(&self) -> [u8; 32] {
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(self.verifying.to_bytes());
|
|
hasher.finalize().into()
|
|
}
|
|
|
|
/// Reconstruct the `SigningKey` from the stored seed bytes.
|
|
fn signing_key(&self) -> SigningKey {
|
|
SigningKey::from_bytes(&self.seed)
|
|
}
|
|
}
|
|
|
|
/// Implement the openmls `Signer` trait so `IdentityKeypair` can be passed
|
|
/// directly to `KeyPackage::builder().build(...)` without needing the external
|
|
/// `openmls_basic_credential` crate.
|
|
impl Signer for IdentityKeypair {
|
|
fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, MlsError> {
|
|
let sk = self.signing_key();
|
|
let sig: ed25519_dalek::Signature = sk.sign(payload);
|
|
Ok(sig.to_bytes().to_vec())
|
|
}
|
|
|
|
fn signature_scheme(&self) -> SignatureScheme {
|
|
SignatureScheme::ED25519
|
|
}
|
|
}
|
|
|
|
impl Serialize for IdentityKeypair {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_bytes(&self.seed[..])
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for IdentityKeypair {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let bytes: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
|
|
let seed: [u8; 32] = bytes
|
|
.as_slice()
|
|
.try_into()
|
|
.map_err(|_| serde::de::Error::custom("identity seed must be 32 bytes"))?;
|
|
Ok(IdentityKeypair::from_seed(seed))
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for IdentityKeypair {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let fp = self.fingerprint();
|
|
f.debug_struct("IdentityKeypair")
|
|
.field(
|
|
"fingerprint",
|
|
&format!("{:02x}{:02x}{:02x}{:02x}…", fp[0], fp[1], fp[2], fp[3]),
|
|
)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|