#[cfg(feature = "dalek")]
pub mod dalek;
#[cfg(feature = "fips")]
pub mod fips;
#[cfg(all(not(feature = "dalek"), not(feature = "fips")))]
compile_error!(
"cellos-core needs a crypto provider: enable the `dalek` (default) or `fips` feature"
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderIdentity {
pub name: &'static str,
pub fips_mode: bool,
pub module_cert: Option<&'static str>,
}
pub trait CryptoProvider: Send + Sync {
fn identity(&self) -> ProviderIdentity;
fn sign_ed25519(&self, seed: &[u8], message: &[u8]) -> Result<[u8; 64], CryptoError>;
fn public_key_from_seed(&self, seed: &[u8]) -> Result<[u8; 32], CryptoError>;
fn validate_ed25519_public_key(&self, public_key: &[u8]) -> Result<(), CryptoError>;
fn verify_ed25519(
&self,
public_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<(), CryptoError>;
fn hmac_sha256(&self, key: &[u8], message: &[u8]) -> [u8; 32];
fn constant_time_eq(&self, a: &[u8], b: &[u8]) -> bool;
fn sha256(&self, message: &[u8]) -> [u8; 32];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TrustAnchorPublicKey([u8; 32]);
impl TrustAnchorPublicKey {
pub fn from_validated_bytes(bytes: [u8; 32]) -> Result<Self, CryptoError> {
provider().validate_ed25519_public_key(&bytes)?;
Ok(Self(bytes))
}
pub fn from_bytes_unchecked(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CryptoError {
BadLength {
what: &'static str,
expected: usize,
got: usize,
},
VerifyFailed(String),
}
impl core::fmt::Display for CryptoError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CryptoError::BadLength {
what,
expected,
got,
} => write!(f, "{what} must be {expected} bytes, got {got}"),
CryptoError::VerifyFailed(msg) => write!(f, "verify failed: {msg}"),
}
}
}
impl std::error::Error for CryptoError {}
impl From<CryptoError> for crate::error::CellosError {
fn from(e: CryptoError) -> Self {
crate::error::CellosError::InvalidSpec(format!("crypto provider: {e}"))
}
}
pub fn provider() -> &'static dyn CryptoProvider {
#[cfg(feature = "fips")]
{
&fips::FipsProvider
}
#[cfg(all(not(feature = "fips"), feature = "dalek"))]
{
&dalek::DalekProvider
}
}
pub fn provider_identity() -> ProviderIdentity {
provider().identity()
}
pub fn power_on_self_test() -> Result<(), CryptoError> {
let p = provider();
let seed = [0x42u8; 32];
let msg = b"cellos-crypto-power-on-self-test";
let sig = p.sign_ed25519(&seed, msg)?;
let public = p.public_key_from_seed(&seed)?;
p.verify_ed25519(&public, msg, &sig)?;
if p.verify_ed25519(&public, b"tampered-post", &sig).is_ok() {
return Err(CryptoError::VerifyFailed(
"power-on self-test: a tampered message verified".into(),
));
}
if p.hmac_sha256(b"post-key", msg) != p.hmac_sha256(b"post-key", msg)
|| p.sha256(msg) != p.sha256(msg)
{
return Err(CryptoError::VerifyFailed(
"power-on self-test: hmac/sha256 not deterministic".into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "fips"))]
#[test]
fn default_provider_is_non_fips() {
let id = provider_identity();
assert!(!id.fips_mode, "default build must not claim FIPS mode");
assert_eq!(id.name, "dalek");
assert_eq!(id.module_cert, None);
}
#[cfg(feature = "fips")]
#[test]
fn fips_feature_selects_fips_provider() {
let id = provider_identity();
assert!(id.fips_mode, "fips build must claim FIPS mode");
assert_eq!(id.name, "aws-lc-fips");
}
#[test]
fn trait_is_object_safe() {
let p: &dyn CryptoProvider = provider();
assert!(!p.identity().name.is_empty());
}
#[test]
fn power_on_self_test_passes_for_active_provider() {
super::power_on_self_test().expect("POST must pass for a working provider");
}
#[test]
fn provider_public_key_from_seed_round_trips() {
let p = provider();
let seed = [0x11u8; 32];
let public = p.public_key_from_seed(&seed).expect("provider derive");
let msg = b"c05-provider-derive";
let sig = p.sign_ed25519(&seed, msg).expect("sign");
p.verify_ed25519(&public, msg, &sig)
.expect("provider-derived key verifies the provider's own signature");
}
#[test]
fn provider_public_key_from_seed_rejects_bad_length() {
assert!(
provider().public_key_from_seed(&[0u8; 31]).is_err(),
"a 31-byte seed must be rejected"
);
}
#[test]
fn provider_sign_verify_property_sweep() {
let p = provider();
for i in 0u32..256 {
let seed = p.sha256(&i.to_le_bytes());
let pk = p.public_key_from_seed(&seed).expect("derive");
let msg = p.sha256(&(i ^ 0xa5a5_a5a5).to_le_bytes());
let sig = p.sign_ed25519(&seed, &msg).expect("sign");
p.verify_ed25519(&pk, &msg, &sig)
.unwrap_or_else(|_| panic!("round-trip must verify (i={i})"));
let mut m2 = msg;
m2[i as usize % 32] ^= 1;
assert!(
p.verify_ed25519(&pk, &m2, &sig).is_err(),
"message tamper must fail (i={i})"
);
let mut s2 = sig;
s2[i as usize % 64] ^= 1;
assert!(
p.verify_ed25519(&pk, &msg, &s2).is_err(),
"signature tamper must fail (i={i})"
);
}
}
#[cfg(not(feature = "fips"))]
#[test]
fn from_validated_bytes_is_strict_on_default_build() {
let p = provider();
let good = p.public_key_from_seed(&[5u8; 32]).unwrap();
assert!(
TrustAnchorPublicKey::from_validated_bytes(good).is_ok(),
"a valid derived key loads"
);
let mut rejected = false;
for i in 0u32..1000 {
let cand = p.sha256(&i.to_le_bytes());
if TrustAnchorPublicKey::from_validated_bytes(cand).is_err() {
rejected = true;
break;
}
}
assert!(
rejected,
"default build must reject some malformed keys at load"
);
}
#[cfg(all(feature = "fips", feature = "dalek"))]
#[test]
fn from_validated_bytes_defers_to_verify_time_under_fips() {
let p = provider();
assert!(p.validate_ed25519_public_key(&[0u8; 31]).is_err());
let mut found_residual = false;
for i in 0u32..1000 {
let cand = p.sha256(&i.to_le_bytes());
if crate::crypto::dalek::validate_ed25519_public_key(&cand).is_err() {
assert!(
TrustAnchorPublicKey::from_validated_bytes(cand).is_ok(),
"fips residual: structurally-invalid key loads (rejection deferred to verify)"
);
found_residual = true;
break;
}
}
assert!(
found_residual,
"expected a dalek-rejected candidate to demonstrate the residual"
);
}
#[test]
fn ed25519_sign_verify_round_trip_through_trait() {
let p = provider();
let seed = [7u8; 32];
let public = p.public_key_from_seed(&seed).expect("derive pub");
let msg = b"canonical-payload-bytes";
let sig = p.sign_ed25519(&seed, msg).expect("sign");
p.verify_ed25519(&public, msg, &sig).expect("verify ok");
assert!(p.verify_ed25519(&public, b"other", &sig).is_err());
}
}