use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use sha2::{Digest, Sha256};
use super::{CryptoError, CryptoProvider, ProviderIdentity};
#[derive(Debug, Clone, Copy, Default)]
pub struct DalekProvider;
impl CryptoProvider for DalekProvider {
fn identity(&self) -> ProviderIdentity {
ProviderIdentity {
name: "dalek",
fips_mode: false,
module_cert: None,
}
}
fn sign_ed25519(&self, seed: &[u8], message: &[u8]) -> Result<[u8; 64], CryptoError> {
let seed_arr: [u8; 32] = seed.try_into().map_err(|_| CryptoError::BadLength {
what: "ed25519 seed",
expected: 32,
got: seed.len(),
})?;
let signing_key = SigningKey::from_bytes(&seed_arr);
let signature = signing_key.sign(message);
Ok(signature.to_bytes())
}
fn public_key_from_seed(&self, seed: &[u8]) -> Result<[u8; 32], CryptoError> {
public_key_from_seed(seed)
}
fn validate_ed25519_public_key(&self, public_key: &[u8]) -> Result<(), CryptoError> {
let arr: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::BadLength {
what: "ed25519 public key",
expected: 32,
got: public_key.len(),
})?;
validate_ed25519_public_key(&arr)
}
fn verify_ed25519(
&self,
public_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<(), CryptoError> {
let pk_arr: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::BadLength {
what: "ed25519 public key",
expected: 32,
got: public_key.len(),
})?;
let sig_arr: [u8; 64] = signature.try_into().map_err(|_| CryptoError::BadLength {
what: "ed25519 signature",
expected: 64,
got: signature.len(),
})?;
let verifying_key = VerifyingKey::from_bytes(&pk_arr)
.map_err(|e| CryptoError::VerifyFailed(format!("invalid ed25519 public key: {e}")))?;
let sig = Signature::from_bytes(&sig_arr);
verifying_key
.verify_strict(message, &sig)
.map_err(|e| CryptoError::VerifyFailed(format!("ed25519 verify failed: {e}")))
}
fn hmac_sha256(&self, key: &[u8], message: &[u8]) -> [u8; 32] {
hmac_sha256(key, message)
}
fn constant_time_eq(&self, a: &[u8], b: &[u8]) -> bool {
constant_time_eq(a, b)
}
fn sha256(&self, message: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(message);
let digest = hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
}
pub fn public_key_from_seed(seed: &[u8]) -> Result<[u8; 32], CryptoError> {
let seed_arr: [u8; 32] = seed.try_into().map_err(|_| CryptoError::BadLength {
what: "ed25519 seed",
expected: 32,
got: seed.len(),
})?;
let signing_key = SigningKey::from_bytes(&seed_arr);
Ok(signing_key.verifying_key().to_bytes())
}
pub fn validate_ed25519_public_key(bytes: &[u8; 32]) -> Result<(), CryptoError> {
VerifyingKey::from_bytes(bytes)
.map(|_| ())
.map_err(|e| CryptoError::VerifyFailed(format!("invalid ed25519 public key: {e}")))
}
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
const BLOCK: usize = 64;
let mut block_key = [0u8; BLOCK];
if key.len() > BLOCK {
let mut hasher = Sha256::new();
hasher.update(key);
let digest = hasher.finalize();
block_key[..32].copy_from_slice(&digest);
} else {
block_key[..key.len()].copy_from_slice(key);
}
let mut ipad = [0u8; BLOCK];
let mut opad = [0u8; BLOCK];
for i in 0..BLOCK {
ipad[i] = block_key[i] ^ 0x36;
opad[i] = block_key[i] ^ 0x5c;
}
let mut inner = Sha256::new();
inner.update(ipad);
inner.update(message);
let inner_digest = inner.finalize();
let mut outer = Sha256::new();
outer.update(opad);
outer.update(inner_digest);
let mac = outer.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&mac);
out
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::trust_keys::{
sign_event_ed25519, verify_signed_event_envelope, SignedEventEnvelopeV1,
};
use crate::types::CloudEventV1;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use std::collections::HashMap;
fn sample_event() -> CloudEventV1 {
CloudEventV1 {
specversion: "1.0".into(),
id: "ev-parity-1".into(),
source: "urn:cellos:test".into(),
ty: "dev.cellos.events.cell.lifecycle.v1.started".into(),
datacontenttype: Some("application/json".into()),
data: Some(serde_json::json!({"cellId": "test-cell-1"})),
time: Some("2026-05-06T12:00:00Z".into()),
traceparent: None,
cex: None,
}
}
#[test]
fn identity_is_non_fips_dalek() {
let id = DalekProvider.identity();
assert_eq!(id.name, "dalek");
assert!(!id.fips_mode);
assert_eq!(id.module_cert, None);
}
#[test]
fn ed25519_sign_matches_legacy_sign_event() {
let seed = [31u8; 32];
let event = sample_event();
let legacy_envelope: SignedEventEnvelopeV1 =
sign_event_ed25519(&event, "ops-event-2026-q2", &seed).expect("legacy sign");
let legacy_sig = legacy_envelope.signature.clone();
let payload = crate::canonical::canonical_payload(&event).expect("payload");
let adapter_sig = DalekProvider
.sign_ed25519(&seed, &payload)
.expect("adapter sign");
let adapter_sig_b64 = URL_SAFE_NO_PAD.encode(adapter_sig);
assert_eq!(
adapter_sig_b64, legacy_sig,
"adapter signature must equal legacy sign_event_ed25519 for fixed input"
);
}
#[test]
fn ed25519_verify_parity_with_envelope_verifier() {
let seed = [42u8; 32];
let event = sample_event();
let public = public_key_from_seed(&seed).unwrap();
let payload = crate::canonical::canonical_payload(&event).unwrap();
let sig = DalekProvider.sign_ed25519(&seed, &payload).unwrap();
DalekProvider
.verify_ed25519(&public, &payload, &sig)
.expect("adapter verify ok");
let envelope = SignedEventEnvelopeV1 {
event: event.clone(),
signer_kid: "k".into(),
algorithm: "ed25519".into(),
signature: URL_SAFE_NO_PAD.encode(sig),
not_before: None,
not_after: None,
};
let mut keys = HashMap::new();
keys.insert(
"k".to_string(),
crate::crypto::TrustAnchorPublicKey::from_validated_bytes(public).unwrap(),
);
let hmac_keys: HashMap<String, Vec<u8>> = HashMap::new();
verify_signed_event_envelope(&envelope, &keys, &hmac_keys).expect("legacy verify ok");
}
#[test]
fn ed25519_verify_rejects_tampered_signature() {
let seed = [9u8; 32];
let public = public_key_from_seed(&seed).unwrap();
let msg = b"hello";
let mut sig = DalekProvider.sign_ed25519(&seed, msg).unwrap();
sig[0] ^= 0xff;
assert!(DalekProvider.verify_ed25519(&public, msg, &sig).is_err());
}
#[test]
fn ed25519_verify_rejects_wrong_length_signature() {
let seed = [9u8; 32];
let public = public_key_from_seed(&seed).unwrap();
let short = [0u8; 10];
let err = DalekProvider
.verify_ed25519(&public, b"hello", &short)
.unwrap_err();
assert!(matches!(
err,
CryptoError::BadLength {
what: "ed25519 signature",
expected: 64,
got: 10
}
));
}
#[test]
fn sign_rejects_wrong_length_seed() {
let err = DalekProvider.sign_ed25519(&[0u8; 5], b"x").unwrap_err();
assert!(matches!(
err,
CryptoError::BadLength {
what: "ed25519 seed",
expected: 32,
got: 5
}
));
}
#[test]
fn hmac_sha256_parity_with_known_construction() {
let key = b"super-secret-shared-symmetric-key";
let msg = b"some canonical payload bytes";
let adapter = DalekProvider.hmac_sha256(key, msg);
let direct = hmac_sha256(key, msg);
assert_eq!(adapter, direct);
let long_key = vec![0xABu8; 100];
assert_eq!(
DalekProvider.hmac_sha256(&long_key, msg),
hmac_sha256(&long_key, msg)
);
}
#[test]
fn hmac_sha256_matches_sign_event_hmac() {
let key = b"shared-key-material";
let event = sample_event();
let envelope =
crate::trust_keys::sign_event_hmac_sha256(&event, "kid", key).expect("hmac sign");
let payload = crate::canonical::canonical_payload(&event).unwrap();
let adapter_mac = DalekProvider.hmac_sha256(key, &payload);
assert_eq!(URL_SAFE_NO_PAD.encode(adapter_mac), envelope.signature);
}
#[test]
fn constant_time_eq_parity() {
assert!(DalekProvider.constant_time_eq(b"abc", b"abc"));
assert!(!DalekProvider.constant_time_eq(b"abc", b"abd"));
assert!(!DalekProvider.constant_time_eq(b"abc", b"ab"));
assert!(DalekProvider.constant_time_eq(b"", b""));
}
#[test]
fn sha256_matches_direct_digest() {
let msg = b"content to hash";
let mut hasher = Sha256::new();
hasher.update(msg);
let expected: [u8; 32] = hasher.finalize().into();
assert_eq!(DalekProvider.sha256(msg), expected);
}
}