General Upgrade, NEW: WebServers, Better Docs
Some checks failed
CI / checks (push) Failing after 3m30s

This commit is contained in:
Alex Emmet 2026-07-18 03:08:03 +02:00
commit 59419f086f
122 changed files with 10122 additions and 4965 deletions

2
crypto/Cargo.lock generated
View file

@ -4,4 +4,4 @@ version = 4
[[package]]
name = "crypto"
version = "0.1.0"
version = "0.2.0"

View file

@ -1,6 +1,6 @@
[package]
name = "mtp-crypto"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
[package.metadata.cargo-machete]
@ -18,11 +18,14 @@ sha2 = { version = "0.11", optional = true }
zeroize = { version = "1.9", features = ["derive"] }
thiserror = "1"
base64 = "0.22"
rand_core = { version = "0.6", features = ["getrandom"] }
rand_core = { version = "0.10.1" }
rand = "0.10.2"
getrandom = "0.4.3"
mlkem-tls = { version = "0.2", optional = true }
ml-dsa = { version = "0.1.1", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
rcgen = { version = "0.14", optional = true }
time = { version = "0.3", optional = true }
[features]
default = ["chacha20poly1305", "ed25519-dalek", "hkdf", "sha2", "ml-dsa"]
@ -32,3 +35,6 @@ full = ["default", "aes-gcm"]
pqc = ["mlkem-tls", "ml-dsa"]
serde = ["dep:serde"]
wasm = ["getrandom/wasm_js"]
hkdf = ["dep:hkdf", "dep:sha2"]
sha2 = ["dep:sha2"]
tls = ["dep:rcgen", "dep:time"]

View file

@ -1,164 +0,0 @@
# mtp-crypto
Cryptographic primitives for the MTP protocol. Classical and post-quantum.
## Features
| Feature | Primitives |
|---------|-----------|
| `default` | XChaCha20-Poly1305, Ed25519, ML-DSA-65, HKDF-SHA-256, SHA-256 |
| `full` | default + AES-256-GCM |
| `pqc` | ML-KEM-768+X25519 hybrid KEM |
ML-DSA-65 is enabled by default so dual-signature support is always available
without a separate PQC feature flag in protocol crates.
## AEAD
XChaCha20-Poly1305 (default) and AES-256-GCM (`full` feature). Nonce is prepended to ciphertext.
```rust
use mtp_crypto::{ChaCha20Poly1305, AeadEncrypt, AeadDecrypt};
let cipher = ChaCha20Poly1305::new([0u8; 32]);
let ct = cipher.encrypt(b"hello", b"aad")?;
let pt = cipher.decrypt(&ct, b"aad")?;
```
## Signatures
### Ed25519
```rust
use mtp_crypto::{Ed25519Signer, SignatureScheme};
let (signer, sk, pk) = Ed25519Signer::generate();
let sig = signer.sign(b"message")?;
signer.verify(b"message", &sig)?;
```
### ML-DSA-65
```rust
use mtp_crypto::{MlDsaSigner, SignatureScheme};
let (signer, sk, pk) = MlDsaSigner::generate();
let sig = signer.sign(b"message")?;
signer.verify(b"message", &sig)?;
// Load from stored bytes
let signer = MlDsaSigner::new(&sk, &pk)?;
```
### Dual signatures
```rust
use mtp_crypto::{sign_dual, DualSignature, Ed25519Signer, MlDsaSigner};
let (ed_signer, _, _) = Ed25519Signer::generate();
let (ml_signer, _, _) = MlDsaSigner::generate();
let dual = sign_dual(ed_signer.signing_key(), ml_signer.signing_key(), b"msg");
dual.verify(ed_signer.verifying_key(), ml_signer.verifying_key(), b"msg")?;
```
## Hybrid KEM
X25519 + ML-KEM-768. 64-byte shared secret. Feed into HKDF before use.
```rust
use mtp_crypto::HybridKem;
let (sk, pk) = HybridKem::generate_keypair();
let enc = HybridKem::encapsulate(&pk)?;
let ss = HybridKem::decapsulate(&sk, &enc.ciphertext)?;
assert_eq!(enc.shared_secret, ss);
```
## Encrypted containers
Self-describing encrypted blobs with algorithm selection via `EncryptionType`.
Each blob begins with a marking byte so recipients can decrypt without
out-of-band agreement.
```rust
use mtp_crypto::{EncryptionType, Keyring, encrypt_for, decrypt_with};
let kr = Keyring::generate();
let blob = encrypt_for(EncryptionType::MlKemChaCha20Poly1305, &kr.public_key_bundle(), b"data", b"aad")?;
let pt = decrypt_with(&blob, &kr, b"aad")?;
```
## Multi-recipient encryption
Encrypt a payload for multiple recipients using a content-encryption key wrapped
per-recipient via Hybrid KEM.
```rust
use mtp_crypto::{Keyring, encrypt_multi, decrypt_multi};
let alice = Keyring::generate();
let bob = Keyring::generate();
let msg = encrypt_multi(b"secret", b"aad", &[alice.public_key_bundle(), bob.public_key_bundle()])?;
let pt = decrypt_multi(&msg, b"aad", &alice)?;
```
## Authentication handshake
Canonical domain-separated payloads for the challenge-response handshake.
```rust
use mtp_crypto::auth::{challenge_payload, login_proof_payload, register_proof_payload, host_final_payload};
```
Each payload type uses a distinct domain tag to prevent replay across protocol steps.
## KDF
```rust
use mtp_crypto::{hkdf_expand, hkdf_extract, derive_encryption_key};
let key = derive_encryption_key(b"ikm", b"salt", b"context")?;
let prk = hkdf_extract(b"ikm", b"salt");
```
## Hashing
```rust
use mtp_crypto::{sha256, sha256_double, Sha256Hasher};
let h = sha256(b"data");
let h2 = sha256_double(b"data");
let mut hasher = Sha256Hasher::new();
hasher.update(b"da");
hasher.update(b"ta");
let h3 = hasher.finalize();
```
## Key types
| Type | Secret | Zeroized |
|------|--------|----------|
| `EncryptionPrivateKey` | KEM/ECDH secret | Yes |
| `EncryptionPublicKey` | KEM/ECDH public | No |
| `SignaturePrivateKey` | Classical signing key | Yes |
| `SignaturePublicKey` | Classical verifying key | No |
| `KemPrivateKey` | Hybrid KEM secret | Yes |
| `KemPublicKey` | Hybrid KEM public | No |
| `SignaturePqPrivateKey` | PQC signing key | Yes |
| `SignaturePqPublicKey` | PQC verifying key | No |
`Keyring` holds all six keys (hybrid KEM + PQ sig + classical sig) plus
`generate()`, `to_bytes()`, and `from_bytes()` for serialization.
`PublicKeyBundle` holds the three public keys for distribution.
## Feature flags
```toml
[dependencies]
mtp-crypto = { path = "../crypto" } # classical + ML-DSA
mtp-crypto = { path = "../crypto", features = ["pqc"] } # adds hybrid KEM
mtp-crypto = { path = "../crypto", features = ["full", "pqc"] } # adds AES-256-GCM + hybrid KEM
mtp-crypto = { path = "../crypto", features = ["serde"] } # serde support
mtp-crypto = { path = "../crypto", features = ["wasm"] } # WASM compat
```

View file

@ -1,10 +1,10 @@
use crate::error::CryptoError;
#[cfg(any(feature = "chacha20poly1305", feature = "aes-gcm"))]
use rand_core::OsRng;
use zeroize::Zeroizing;
#[cfg(any(feature = "chacha20poly1305", feature = "aes-gcm"))]
use rand_core::RngCore;
use getrandom::fill;
pub trait AeadEncrypt {
fn encrypt(&self, plaintext: &[u8], aad: &[u8]) -> Result<Vec<u8>, CryptoError>;
@ -28,13 +28,15 @@ fn prepend_nonce(nonce: &[u8], ciphertext: &mut Vec<u8>) -> Vec<u8> {
#[cfg(feature = "chacha20poly1305")]
pub struct ChaCha20Poly1305 {
key: [u8; 32],
key: Zeroizing<[u8; 32]>,
}
#[cfg(feature = "chacha20poly1305")]
impl ChaCha20Poly1305 {
pub fn new(key: [u8; 32]) -> Self {
Self { key }
Self {
key: Zeroizing::new(key),
}
}
}
@ -45,11 +47,11 @@ impl AeadEncrypt for ChaCha20Poly1305 {
use chacha20poly1305::XNonce;
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
let key = chacha20poly1305::Key::from_slice(&self.key);
let key = chacha20poly1305::Key::from_slice(self.key.as_ref());
let cipher = XChaCha20Poly1305::new(key);
let mut nonce = [0u8; 24];
OsRng.fill_bytes(&mut nonce);
fill(&mut nonce).map_err(|_| CryptoError::EncryptionFailed)?;
let nonce_ref = XNonce::from_slice(&nonce);
let payload = Payload {
@ -77,7 +79,7 @@ impl AeadDecrypt for ChaCha20Poly1305 {
}
let (nonce, ct) = ciphertext.split_at(24);
let key = chacha20poly1305::Key::from_slice(&self.key);
let key = chacha20poly1305::Key::from_slice(self.key.as_ref());
let cipher = XChaCha20Poly1305::new(key);
let nonce_ref = XNonce::from_slice(nonce);
@ -98,13 +100,15 @@ impl AeadCipher for ChaCha20Poly1305 {
#[cfg(feature = "aes-gcm")]
pub struct Aes256Gcm {
key: [u8; 32],
key: Zeroizing<[u8; 32]>,
}
#[cfg(feature = "aes-gcm")]
impl Aes256Gcm {
pub fn new(key: [u8; 32]) -> Self {
Self { key }
Self {
key: Zeroizing::new(key),
}
}
}
@ -115,11 +119,11 @@ impl AeadEncrypt for Aes256Gcm {
use aes_gcm::Nonce;
use aes_gcm::aead::{Aead, KeyInit, Payload};
let key = aes_gcm::Key::<AesGcmInner>::from_slice(&self.key);
let key = aes_gcm::Key::<AesGcmInner>::from_slice(self.key.as_ref());
let cipher = AesGcmInner::new(key);
let mut nonce = [0u8; 12];
OsRng.fill_bytes(&mut nonce);
fill(&mut nonce).map_err(|_| CryptoError::EncryptionFailed)?;
let nonce_ref = Nonce::from_slice(&nonce);
let payload = Payload {
@ -147,7 +151,7 @@ impl AeadDecrypt for Aes256Gcm {
}
let (nonce, ct) = ciphertext.split_at(12);
let key = aes_gcm::Key::<AesGcmInner>::from_slice(&self.key);
let key = aes_gcm::Key::<AesGcmInner>::from_slice(self.key.as_ref());
let cipher = AesGcmInner::new(key);
let nonce_ref = Nonce::from_slice(nonce);

View file

@ -11,7 +11,7 @@
* Step 4. Host -> Client : IdentificationResponse { connected, id, host_sig } host_sig over host_final_payload
*/
/// Domain-separation tags — a distinct leading byte per signed context.
/// Domain-separation tags
pub mod domain {
/// Host's signature over the challenge it issues (step 2).
pub const CHALLENGE: u8 = 0x10;

View file

@ -30,4 +30,6 @@ pub enum CryptoError {
InvalidHex,
#[error("invalid base64 encoding")]
InvalidBase64,
#[error("TLS error: {0}")]
Tls(String),
}

View file

@ -9,7 +9,9 @@ use crate::kem::HybridKem;
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
use crate::keypair::{Keyring, PublicKeyBundle};
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
use rand_core::RngCore;
use rand::Rng;
#[cfg(all(feature = "mlkem-tls", feature = "chacha20poly1305", feature = "hkdf"))]
use zeroize::Zeroizing;
pub struct RecipientEntry {
pub kem_ciphertext: Vec<u8>,
@ -126,10 +128,10 @@ pub fn encrypt_multi(
aad: &[u8],
entities: &[PublicKeyBundle],
) -> Result<MultiEncryptedMessage, CryptoError> {
let mut cek = [0u8; 32];
rand_core::OsRng.fill_bytes(&mut cek);
let mut cek = Zeroizing::new([0u8; 32]);
rand::rng().fill_bytes(cek.as_mut());
let cipher = ChaCha20Poly1305::new(cek);
let cipher = ChaCha20Poly1305::new(*cek);
let encrypted_payload = cipher.encrypt(plaintext, aad)?;
let nonce: [u8; 24] = encrypted_payload[..24]
@ -140,14 +142,14 @@ pub fn encrypt_multi(
let mut recipients = Vec::with_capacity(entities.len());
for entity in entities {
let enc = HybridKem::encapsulate(&entity.kem_public_key)?;
let wrap_key = derive_encryption_key(
let wrap_key = Zeroizing::new(derive_encryption_key(
&enc.shared_secret,
b"mtp-multi-key-wrap",
b"multi-recipient",
)?;
)?);
let wrap_cipher = ChaCha20Poly1305::new(wrap_key);
let encrypted_key = wrap_cipher.encrypt(&cek, b"")?;
let wrap_cipher = ChaCha20Poly1305::new(*wrap_key);
let encrypted_key = wrap_cipher.encrypt(cek.as_ref(), b"")?;
recipients.push(RecipientEntry {
kem_ciphertext: enc.ciphertext,
@ -179,19 +181,27 @@ pub fn decrypt_multi(
Ok(s) => s,
Err(_) => continue,
};
let wrap_key = derive_encryption_key(&ss, b"mtp-multi-key-wrap", b"multi-recipient")?;
let wrap_cipher = ChaCha20Poly1305::new(wrap_key);
let wrap_key = Zeroizing::new(derive_encryption_key(
&ss,
b"mtp-multi-key-wrap",
b"multi-recipient",
)?);
let wrap_cipher = ChaCha20Poly1305::new(*wrap_key);
let cek = match wrap_cipher.decrypt(&entry.encrypted_key, b"") {
Ok(k) => k,
Ok(k) => Zeroizing::new(k),
Err(_) => continue,
};
let cek_arr: [u8; 32] = cek.try_into().map_err(|_| CryptoError::DecryptionFailed)?;
let cek_arr = Zeroizing::new(
cek.as_slice()
.try_into()
.map_err(|_| CryptoError::DecryptionFailed)?,
);
let mut full_ct = Vec::with_capacity(24 + msg.ciphertext.len());
full_ct.extend_from_slice(&msg.nonce);
full_ct.extend_from_slice(&msg.ciphertext);
let data_cipher = ChaCha20Poly1305::new(cek_arr);
let data_cipher = ChaCha20Poly1305::new(*cek_arr);
return data_cipher.decrypt(&full_ct, aad);
}
Err(CryptoError::DecryptionFailed)

View file

@ -1,9 +1,10 @@
use crate::error::CryptoError;
use crate::keypair::{KemPrivateKey, KemPublicKey};
use zeroize::Zeroizing;
pub struct Encapsulated {
pub ciphertext: Vec<u8>,
pub shared_secret: Vec<u8>,
pub shared_secret: Zeroizing<Vec<u8>>,
}
#[cfg(feature = "mlkem-tls")]
@ -12,7 +13,10 @@ pub struct HybridKem;
#[cfg(feature = "mlkem-tls")]
impl HybridKem {
pub fn generate_keypair() -> (KemPrivateKey, KemPublicKey) {
let (ek, dk) = mlkem_tls::X25519MlKem768::keygen(&mut rand_core::OsRng);
/* Obviously: cannot find module or crate rand_core06 in this scope
use of unresolved module or unlinked crate rand_core06 (rustc E0433) */
let (ek, dk) =
mlkem_tls::X25519MlKem768::keygen(&mut chacha20poly1305::aead::rand_core::OsRng);
(
KemPrivateKey::new(dk.as_bytes().to_vec()),
KemPublicKey::new(ek.as_bytes().to_vec()),
@ -22,22 +26,25 @@ impl HybridKem {
pub fn encapsulate(recipient_pk: &KemPublicKey) -> Result<Encapsulated, CryptoError> {
let ek = mlkem_tls::EncapsKey768::try_from(recipient_pk.as_bytes())
.map_err(|_| CryptoError::KemEncapsulationFailed)?;
let (ct, ss) = mlkem_tls::X25519MlKem768::encapsulate(&ek, &mut rand_core::OsRng);
let (ct, ss) = mlkem_tls::X25519MlKem768::encapsulate(
&ek,
&mut chacha20poly1305::aead::rand_core::OsRng,
);
Ok(Encapsulated {
ciphertext: ct.as_bytes().to_vec(),
shared_secret: ss.as_bytes().to_vec(),
shared_secret: Zeroizing::new(ss.as_bytes().to_vec()),
})
}
pub fn decapsulate(
recipient_sk: &KemPrivateKey,
ciphertext: &[u8],
) -> Result<Vec<u8>, CryptoError> {
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let dk = mlkem_tls::DecapsKey768::try_from(recipient_sk.as_bytes())
.map_err(|_| CryptoError::KemDecapsulationFailed)?;
let ct = mlkem_tls::Ciphertext768Hybrid::try_from(ciphertext)
.map_err(|_| CryptoError::KemDecapsulationFailed)?;
let ss = mlkem_tls::X25519MlKem768::decapsulate(&dk, &ct);
Ok(ss.as_bytes().to_vec())
Ok(Zeroizing::new(ss.as_bytes().to_vec()))
}
}

View file

@ -2,179 +2,59 @@ use std::fmt;
use base64::Engine;
use base64::engine::general_purpose;
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
// --- Private key types ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct EncryptionPrivateKey(Vec<u8>);
macro_rules! impl_private_key {
($name:ident) => {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct $name(Vec<u8>);
impl EncryptionPrivateKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
impl $name {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(stringify!($name))
.field("len", &self.0.len())
.field("data", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for $name {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for $name {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for $name {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
};
}
impl fmt::Debug for EncryptionPrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EncryptionPrivateKey")
.field("len", &self.0.len())
.field("data", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for EncryptionPrivateKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for EncryptionPrivateKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for EncryptionPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SignaturePrivateKey(Vec<u8>);
impl SignaturePrivateKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for SignaturePrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SignaturePrivateKey")
.field("len", &self.0.len())
.field("data", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for SignaturePrivateKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for SignaturePrivateKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for SignaturePrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct KemPrivateKey(Vec<u8>);
impl KemPrivateKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for KemPrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KemPrivateKey")
.field("len", &self.0.len())
.field("data", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for KemPrivateKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for KemPrivateKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for KemPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SignaturePqPrivateKey(Vec<u8>);
impl SignaturePqPrivateKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl fmt::Debug for SignaturePqPrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SignaturePqPrivateKey")
.field("len", &self.0.len())
.field("data", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for SignaturePqPrivateKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for SignaturePqPrivateKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for SignaturePqPrivateKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl_private_key!(EncryptionPrivateKey);
impl_private_key!(SignaturePrivateKey);
impl_private_key!(KemPrivateKey);
impl_private_key!(SignaturePqPrivateKey);
// --- Public key types ---
@ -210,213 +90,64 @@ fn base64_to_bytes(s: &str) -> Result<Vec<u8>, crate::error::CryptoError> {
.map_err(|_| crate::error::CryptoError::InvalidBase64)
}
// ---
macro_rules! impl_public_key {
($name:ident) => {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub struct $name(Vec<u8>);
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub struct EncryptionPublicKey(Vec<u8>);
impl $name {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
hex_to_bytes(s).map(Self)
}
}
impl EncryptionPublicKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
hex_to_bytes(s).map(Self)
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", stringify!($name), self.to_hex())
}
}
impl AsRef<[u8]> for $name {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for $name {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for $name {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl From<&$name> for Vec<u8> {
fn from(key: &$name) -> Vec<u8> {
key.0.clone()
}
}
};
}
impl fmt::Debug for EncryptionPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "EncryptionPublicKey({})", self.to_hex())
}
}
impl AsRef<[u8]> for EncryptionPublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for EncryptionPublicKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for EncryptionPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl From<&EncryptionPublicKey> for Vec<u8> {
fn from(key: &EncryptionPublicKey) -> Vec<u8> {
key.0.clone()
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub struct SignaturePublicKey(Vec<u8>);
impl SignaturePublicKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
hex_to_bytes(s).map(Self)
}
}
impl fmt::Debug for SignaturePublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SignaturePublicKey({})", self.to_hex())
}
}
impl AsRef<[u8]> for SignaturePublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for SignaturePublicKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for SignaturePublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl From<&SignaturePublicKey> for Vec<u8> {
fn from(key: &SignaturePublicKey) -> Vec<u8> {
key.0.clone()
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub struct KemPublicKey(Vec<u8>);
impl KemPublicKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
hex_to_bytes(s).map(Self)
}
}
impl fmt::Debug for KemPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "KemPublicKey({})", self.to_hex())
}
}
impl AsRef<[u8]> for KemPublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for KemPublicKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for KemPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl From<&KemPublicKey> for Vec<u8> {
fn from(key: &KemPublicKey) -> Vec<u8> {
key.0.clone()
}
}
// ---
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(Clone)]
pub struct SignaturePqPublicKey(Vec<u8>);
impl SignaturePqPublicKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_hex(&self) -> String {
bytes_to_hex(&self.0)
}
pub fn from_hex(s: &str) -> Result<Self, crate::error::CryptoError> {
hex_to_bytes(s).map(Self)
}
}
impl fmt::Debug for SignaturePqPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SignaturePqPublicKey({})", self.to_hex())
}
}
impl AsRef<[u8]> for SignaturePqPublicKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
impl From<Vec<u8>> for SignaturePqPublicKey {
fn from(bytes: Vec<u8>) -> Self {
Self(bytes)
}
}
impl From<&[u8]> for SignaturePqPublicKey {
fn from(bytes: &[u8]) -> Self {
Self(bytes.to_vec())
}
}
impl From<&SignaturePqPublicKey> for Vec<u8> {
fn from(key: &SignaturePqPublicKey) -> Vec<u8> {
key.0.clone()
}
}
impl_public_key!(EncryptionPublicKey);
impl_public_key!(SignaturePublicKey);
impl_public_key!(KemPublicKey);
impl_public_key!(SignaturePqPublicKey);
// --- Keyring ---
@ -477,7 +208,7 @@ impl Keyring {
}
}
pub fn to_bytes(&self) -> Vec<u8> {
pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
let fields: &[&[u8]] = &[
self.kem_public_key.as_bytes(),
self.kem_secret_key.as_bytes(),
@ -486,7 +217,7 @@ impl Keyring {
self.sig_cl_public_key.as_bytes(),
self.sig_cl_secret_key.as_bytes(),
];
let mut out = Vec::new();
let mut out = Zeroizing::new(Vec::new());
for f in fields {
out.extend_from_slice(&(f.len() as u16).to_be_bytes());
out.extend_from_slice(f);
@ -549,12 +280,6 @@ impl TryFrom<&[u8]> for Keyring {
}
}
impl From<&Keyring> for Vec<u8> {
fn from(keyring: &Keyring) -> Vec<u8> {
keyring.to_bytes()
}
}
impl fmt::Debug for Keyring {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Keyring")
@ -787,7 +512,7 @@ mod tests {
SignaturePublicKey::new(vec![4u8; 16]),
SignaturePrivateKey::new(vec![5u8; 16]),
);
let bytes: Vec<u8> = Vec::from(&keyring);
let bytes = keyring.to_bytes();
let recovered = Keyring::try_from(bytes.as_slice())?;
assert_eq!(keyring.to_bytes(), recovered.to_bytes());
Ok(())

View file

@ -22,6 +22,9 @@ pub mod enc;
pub mod helper;
#[cfg(feature = "tls")]
pub mod tls;
pub use aead::{AeadCipher, AeadDecrypt, AeadEncrypt};
pub use error::CryptoError;
pub use keypair::{
@ -85,7 +88,9 @@ mod tests {
use crate::aead::{AeadDecrypt, AeadEncrypt, ChaCha20Poly1305};
let cipher_a = ChaCha20Poly1305::new([0xAB; 32]);
let cipher_b = ChaCha20Poly1305::new([0xCD; 32]);
let ct = cipher_a.encrypt(b"hello", b"").expect("encryption should succeed");
let ct = cipher_a
.encrypt(b"hello", b"")
.expect("encryption should succeed");
assert!(cipher_b.decrypt(&ct, b"").is_err());
}
@ -106,11 +111,15 @@ mod tests {
let (signer, sk, pk) = Ed25519Signer::generate();
let msg = b"test message";
let sig = signer.sign(msg).expect("signing should succeed");
signer.verify(msg, &sig).expect("verification should succeed");
signer
.verify(msg, &sig)
.expect("verification should succeed");
verify_ed25519(&pk, msg, &sig).expect("verification should succeed");
let loaded = Ed25519Signer::new(&sk).expect("signer loading should succeed");
loaded.verify(msg, &sig).expect("verification should succeed");
loaded
.verify(msg, &sig)
.expect("verification should succeed");
}
#[cfg(feature = "ed25519-dalek")]
@ -128,11 +137,15 @@ mod tests {
let (signer, sk, pk) = MlDsaSigner::generate();
let msg = b"test message";
let sig = signer.sign(msg).expect("signing should succeed");
signer.verify(msg, &sig).expect("verification should succeed");
signer
.verify(msg, &sig)
.expect("verification should succeed");
verify_ml_dsa(&pk, msg, &sig).expect("verification should succeed");
let loaded = MlDsaSigner::new(&sk, &pk).expect("signer loading should succeed");
loaded.verify(msg, &sig).expect("verification should succeed");
loaded
.verify(msg, &sig)
.expect("verification should succeed");
}
#[cfg(feature = "ml-dsa")]
@ -183,8 +196,8 @@ mod tests {
.expect("key derivation should succeed");
assert_eq!(key.len(), 32);
let expanded = hkdf_expand(b"ikm", b"salt", b"info", 64)
.expect("HKDF expansion should succeed");
let expanded =
hkdf_expand(b"ikm", b"salt", b"info", 64).expect("HKDF expansion should succeed");
assert_eq!(expanded.len(), 64);
}
@ -285,8 +298,7 @@ mod tests {
let kr = Keyring::generate();
let bundle = kr.public_key_bundle();
let bytes = bundle.as_bytes();
let loaded =
PublicKeyBundle::from_bytes(&bytes).expect("bundle roundtrip should succeed");
let loaded = PublicKeyBundle::from_bytes(&bytes).expect("bundle roundtrip should succeed");
assert_eq!(
bundle.kem_public_key.as_bytes(),
loaded.kem_public_key.as_bytes()
@ -306,7 +318,8 @@ mod tests {
fn hybrid_kem_roundtrip() {
let (sk, pk) = HybridKem::generate_keypair();
let enc = HybridKem::encapsulate(&pk).expect("encapsulation should succeed");
let ss = HybridKem::decapsulate(&sk, &enc.ciphertext).expect("decapsulation should succeed");
let ss =
HybridKem::decapsulate(&sk, &enc.ciphertext).expect("decapsulation should succeed");
assert_eq!(enc.shared_secret, ss);
}

View file

@ -21,9 +21,6 @@ impl SigAlgorithm {
}
}
#[cfg(feature = "ed25519-dalek")]
use rand_core::RngCore;
#[cfg(feature = "ml-dsa")]
use crate::keypair::{SignaturePqPrivateKey, SignaturePqPublicKey};
@ -50,9 +47,12 @@ impl Ed25519Signer {
Ok(Self { secret, public })
}
#[cfg(feature = "ed25519-dalek")]
pub fn generate() -> (Self, SignaturePrivateKey, SignaturePublicKey) {
use rand::RngExt;
let mut bytes = [0u8; 32];
rand_core::OsRng.fill_bytes(&mut bytes);
rand::rng().fill(&mut bytes);
let secret = ed25519_dalek::SigningKey::from_bytes(&bytes);
let public = secret.verifying_key();
let priv_key = SignaturePrivateKey::new(secret.to_bytes().to_vec());

45
crypto/src/tls.rs Normal file
View file

@ -0,0 +1,45 @@
use rcgen::{CertificateParams, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use time::{Duration, OffsetDateTime};
use crate::CryptoError;
/// Generate a self-signed TLS certificate and private key for development.
///
/// Returns `(cert_pem, key_pem)` as byte vectors. The certificate is valid for
/// the given domain name plus `127.0.0.1` and `::1`, uses ECDSA P-256, and is
/// valid for 13 days from the time of generation.
///
/// Never panics; all errors are returned as [`CryptoError`].
pub fn generate_self_signed_cert(domain: &str) -> Result<(Vec<u8>, Vec<u8>), CryptoError> {
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)
.map_err(|e| CryptoError::Tls(format!("key generation failed: {e}")))?;
let mut params = CertificateParams::new(vec![domain.to_string()])
.map_err(|e| CryptoError::Tls(format!("certificate params failed: {e}")))?;
params.not_before = OffsetDateTime::now_utc() - Duration::minutes(5);
params.not_after = OffsetDateTime::now_utc() + Duration::days(13);
params
.subject_alt_names
.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
params
.subject_alt_names
.push(SanType::IpAddress(IpAddr::V6(Ipv6Addr::new(
0, 0, 0, 0, 0, 0, 0, 1,
))));
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
params.is_ca = IsCa::NoCa;
let cert = params
.self_signed(&key_pair)
.map_err(|e| CryptoError::Tls(format!("certificate signing failed: {e}")))?;
let cert_pem = cert.pem().into_bytes();
let key_pem = key_pair.serialize_pem().into_bytes();
Ok((cert_pem, key_pem))
}