#![allow(clippy::disallowed_methods)]
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::provider::{CryptoError, CurveType, SecureSeed};
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub enum TypedSeed {
Ed25519(#[zeroize] [u8; 32]),
P256(#[zeroize] [u8; 32]),
}
impl TypedSeed {
pub fn curve(&self) -> CurveType {
match self {
Self::Ed25519(_) => CurveType::Ed25519,
Self::P256(_) => CurveType::P256,
}
}
pub fn as_bytes(&self) -> &[u8; 32] {
match self {
Self::Ed25519(b) | Self::P256(b) => b,
}
}
pub fn to_secure_seed(&self) -> SecureSeed {
SecureSeed::new(*self.as_bytes())
}
}
impl std::fmt::Debug for TypedSeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ed25519(_) => f.write_str("TypedSeed::Ed25519([REDACTED])"),
Self::P256(_) => f.write_str("TypedSeed::P256([REDACTED])"),
}
}
}
#[derive(Debug)]
pub struct ParsedKey {
pub seed: TypedSeed,
pub public_key: Vec<u8>,
}
pub fn parse_key_material(bytes: &[u8]) -> Result<ParsedKey, CryptoError> {
if let Ok((seed, maybe_pk)) = crate::key_material::parse_ed25519_key_material(bytes) {
let public_key = match maybe_pk {
Some(pk) => pk.to_vec(),
None => {
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
{
use ring::signature::{Ed25519KeyPair, KeyPair};
let kp = Ed25519KeyPair::from_seed_unchecked(seed.as_bytes()).map_err(|e| {
CryptoError::OperationFailed(format!("Ed25519 pubkey: {e}"))
})?;
kp.public_key().as_ref().to_vec()
}
#[cfg(not(all(feature = "native", not(target_arch = "wasm32"))))]
{
return Err(CryptoError::UnsupportedTarget);
}
}
};
return Ok(ParsedKey {
seed: TypedSeed::Ed25519(*seed.as_bytes()),
public_key,
});
}
#[cfg(feature = "native")]
{
use p256::pkcs8::DecodePrivateKey;
if let Ok(sk) = p256::ecdsa::SigningKey::from_pkcs8_der(bytes) {
let vk = p256::ecdsa::VerifyingKey::from(&sk);
let compressed = vk.to_encoded_point(true);
let mut scalar = [0u8; 32];
scalar.copy_from_slice(&sk.to_bytes());
return Ok(ParsedKey {
seed: TypedSeed::P256(scalar),
public_key: compressed.as_bytes().to_vec(),
});
}
}
Err(CryptoError::InvalidPrivateKey(format!(
"Unrecognized key format ({} bytes)",
bytes.len()
)))
}
#[cfg(all(feature = "fips", not(target_arch = "wasm32")))]
use crate::aws_lc_provider::AwsLcProvider as SyncProvider;
#[cfg(all(feature = "cnsa", not(feature = "fips"), not(target_arch = "wasm32")))]
use crate::cnsa_provider::CnsaProvider as SyncProvider;
#[cfg(all(
feature = "native",
not(feature = "fips"),
not(feature = "cnsa"),
not(target_arch = "wasm32")
))]
use crate::ring_provider::RingCryptoProvider as SyncProvider;
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
pub fn sign(seed: &TypedSeed, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
match seed {
TypedSeed::Ed25519(s) => SyncProvider::ed25519_sign(s, message),
TypedSeed::P256(s) => SyncProvider::p256_sign(s, message),
}
}
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
pub fn public_key(seed: &TypedSeed) -> Result<Vec<u8>, CryptoError> {
match seed {
TypedSeed::Ed25519(s) => Ok(SyncProvider::ed25519_public_key(s)?.to_vec()),
TypedSeed::P256(s) => SyncProvider::p256_public_key_from_seed(s),
}
}
#[derive(Debug)]
pub struct TypedSignerKey {
seed: TypedSeed,
public_key: Vec<u8>,
}
impl zeroize::ZeroizeOnDrop for TypedSignerKey {}
impl TypedSignerKey {
pub fn from_pkcs8(bytes: &[u8]) -> Result<Self, CryptoError> {
let parsed = parse_key_material(bytes)?;
Ok(Self {
seed: parsed.seed,
public_key: parsed.public_key,
})
}
pub fn from_parts(seed: TypedSeed, public_key: Vec<u8>) -> Result<Self, CryptoError> {
let expected = seed.curve().public_key_len();
if public_key.len() != expected {
return Err(CryptoError::InvalidPrivateKey(format!(
"public key length {} does not match {} expected {} bytes",
public_key.len(),
seed.curve(),
expected
)));
}
Ok(Self { seed, public_key })
}
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
pub fn from_seed(seed: TypedSeed) -> Result<Self, CryptoError> {
let pk = public_key(&seed)?;
Ok(Self {
seed,
public_key: pk,
})
}
pub fn cesr_encoded_pubkey(&self) -> String {
use cesride::Matter;
let code = match self.seed.curve() {
CurveType::Ed25519 => cesride::matter::Codex::Ed25519,
CurveType::P256 => cesride::matter::Codex::ECDSA_256r1,
};
#[allow(clippy::expect_used)]
cesride::Verfer::new(Some(code), Some(&self.public_key), None, None, None)
.and_then(|v| v.qb64())
.expect("cesride verkey encode is infallible for a validated key")
}
pub fn cesr_encoded(&self) -> String {
self.cesr_encoded_pubkey()
}
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
pub fn to_pkcs8(&self) -> Result<crate::pkcs8::Pkcs8Der, CryptoError> {
match &self.seed {
TypedSeed::Ed25519(seed_bytes) => {
if self.public_key.len() != crate::provider::ED25519_PUBLIC_KEY_LEN {
return Err(CryptoError::InvalidPrivateKey(
"Ed25519 public key must be 32 bytes".to_string(),
));
}
let mut pk = [0u8; 32];
pk.copy_from_slice(&self.public_key);
let bytes = crate::key_material::build_ed25519_pkcs8_v2(seed_bytes, &pk);
Ok(crate::pkcs8::Pkcs8Der::new(bytes))
}
TypedSeed::P256(scalar) => {
use p256::ecdsa::SigningKey;
use p256::pkcs8::EncodePrivateKey;
let sk = SigningKey::from_slice(scalar)
.map_err(|e| CryptoError::InvalidPrivateKey(format!("P-256 scalar: {e}")))?;
let doc = sk
.to_pkcs8_der()
.map_err(|e| CryptoError::OperationFailed(format!("P-256 PKCS8: {e}")))?;
Ok(crate::pkcs8::Pkcs8Der::new(doc.as_bytes().to_vec()))
}
}
}
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
pub fn sign(&self, message: &[u8]) -> Result<Vec<u8>, CryptoError> {
sign(&self.seed, message)
}
pub fn curve(&self) -> CurveType {
self.seed.curve()
}
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
pub fn seed(&self) -> &TypedSeed {
&self.seed
}
}
pub fn normalize_verkey(bytes: &[u8], curve: CurveType) -> Result<Vec<u8>, CryptoError> {
match curve {
CurveType::Ed25519 => {
if bytes.len() != 32 {
return Err(CryptoError::OperationFailed(format!(
"Ed25519 verkey must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(bytes.to_vec())
}
CurveType::P256 => {
#[cfg(feature = "native")]
{
use p256::elliptic_curve::sec1::ToEncodedPoint;
let pk = p256::PublicKey::from_sec1_bytes(bytes).map_err(|e| {
CryptoError::OperationFailed(format!("invalid P-256 public key: {e}"))
})?;
Ok(pk.to_encoded_point(true).as_bytes().to_vec())
}
#[cfg(not(feature = "native"))]
{
let _ = bytes;
Err(CryptoError::UnsupportedTarget)
}
}
#[allow(unreachable_patterns)]
other => Err(CryptoError::OperationFailed(format!(
"normalize_verkey: unsupported curve {other:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn typed_seed_curve_identification() {
let ed = TypedSeed::Ed25519([1u8; 32]);
assert_eq!(ed.curve(), CurveType::Ed25519);
let p = TypedSeed::P256([2u8; 32]);
assert_eq!(p.curve(), CurveType::P256);
}
#[test]
fn typed_seed_as_bytes() {
let seed = TypedSeed::Ed25519([42u8; 32]);
assert_eq!(seed.as_bytes(), &[42u8; 32]);
}
#[test]
fn typed_seed_debug_redacts() {
let seed = TypedSeed::P256([0u8; 32]);
let debug = format!("{:?}", seed);
assert!(debug.contains("REDACTED"));
assert!(!debug.contains("0, 0, 0"));
}
#[cfg(all(feature = "native", not(target_arch = "wasm32")))]
mod native {
use super::*;
#[test]
fn parse_ed25519_pkcs8_v2() {
use ring::rand::SystemRandom;
use ring::signature::Ed25519KeyPair;
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
assert_eq!(parsed.public_key.len(), 32);
}
#[test]
fn parse_p256_pkcs8() {
use p256::ecdsa::SigningKey;
use p256::elliptic_curve::rand_core::OsRng;
use p256::pkcs8::EncodePrivateKey;
let sk = SigningKey::random(&mut OsRng);
let pkcs8 = sk.to_pkcs8_der().unwrap();
let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
assert_eq!(parsed.seed.curve(), CurveType::P256);
assert_eq!(parsed.public_key.len(), 33);
}
#[test]
fn parse_raw_32_bytes_is_ed25519() {
let raw = [7u8; 32];
let parsed = parse_key_material(&raw).unwrap();
assert_eq!(parsed.seed.curve(), CurveType::Ed25519);
}
#[test]
fn parse_garbage_fails() {
let garbage = [0xFFu8; 50];
assert!(parse_key_material(&garbage).is_err());
}
#[test]
fn parse_empty_fails() {
assert!(parse_key_material(&[]).is_err());
}
#[test]
fn sign_ed25519_roundtrip() {
use ring::signature::{ED25519, UnparsedPublicKey};
let seed = TypedSeed::Ed25519([1u8; 32]);
let msg = b"hello world";
let sig = sign(&seed, msg).unwrap();
assert_eq!(sig.len(), 64);
let pk = public_key(&seed).unwrap();
let verifier = UnparsedPublicKey::new(&ED25519, &pk);
assert!(verifier.verify(msg, &sig).is_ok());
}
#[test]
fn sign_p256_roundtrip() {
use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
let seed = TypedSeed::P256([3u8; 32]);
let msg = b"hello p256";
let sig_bytes = sign(&seed, msg).unwrap();
assert_eq!(sig_bytes.len(), 64);
let pk_bytes = public_key(&seed).unwrap();
assert_eq!(pk_bytes.len(), 33);
let vk = VerifyingKey::from_sec1_bytes(&pk_bytes).unwrap();
let sig = Signature::from_slice(&sig_bytes).unwrap();
assert!(vk.verify(msg, &sig).is_ok());
}
#[test]
fn cross_curve_isolation() {
let bytes = [5u8; 32];
let ed_seed = TypedSeed::Ed25519(bytes);
let p256_seed = TypedSeed::P256(bytes);
let ed_pk = public_key(&ed_seed).unwrap();
let p256_pk = public_key(&p256_seed).unwrap();
assert_ne!(ed_pk.len(), p256_pk.len());
let msg = b"test";
let ed_sig = sign(&ed_seed, msg).unwrap();
let p256_sig = sign(&p256_seed, msg).unwrap();
assert_eq!(ed_sig.len(), 64);
assert_eq!(p256_sig.len(), 64);
assert_ne!(ed_sig, p256_sig);
}
#[test]
fn parse_then_sign_ed25519() {
use ring::rand::SystemRandom;
use ring::signature::{ED25519, Ed25519KeyPair, UnparsedPublicKey};
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let parsed = parse_key_material(pkcs8.as_ref()).unwrap();
let msg = b"end to end";
let sig = sign(&parsed.seed, msg).unwrap();
let verifier = UnparsedPublicKey::new(&ED25519, &parsed.public_key);
assert!(verifier.verify(msg, &sig).is_ok());
}
#[test]
fn parse_then_sign_p256() {
use p256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier};
use p256::elliptic_curve::rand_core::OsRng;
use p256::pkcs8::EncodePrivateKey;
let sk = SigningKey::random(&mut OsRng);
let pkcs8 = sk.to_pkcs8_der().unwrap();
let parsed = parse_key_material(pkcs8.as_bytes()).unwrap();
let msg = b"end to end p256";
let sig_bytes = sign(&parsed.seed, msg).unwrap();
let vk = VerifyingKey::from_sec1_bytes(&parsed.public_key).unwrap();
let sig = Signature::from_slice(&sig_bytes).unwrap();
assert!(vk.verify(msg, &sig).is_ok());
}
#[test]
fn typed_signer_key_ed25519_roundtrip() {
use ring::rand::SystemRandom;
use ring::signature::Ed25519KeyPair;
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
assert_eq!(s.curve(), CurveType::Ed25519);
assert!(s.cesr_encoded_pubkey().starts_with('D'));
assert_eq!(s.public_key().len(), 32);
let sig = s.sign(b"msg").unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn typed_signer_key_p256_roundtrip() {
use p256::ecdsa::SigningKey;
use p256::elliptic_curve::rand_core::OsRng;
use p256::pkcs8::EncodePrivateKey;
let sk = SigningKey::random(&mut OsRng);
let pkcs8 = sk.to_pkcs8_der().unwrap();
let s = TypedSignerKey::from_pkcs8(pkcs8.as_bytes()).unwrap();
assert_eq!(s.curve(), CurveType::P256);
assert!(s.cesr_encoded_pubkey().starts_with("1AAJ"));
assert_eq!(s.public_key().len(), 33);
let sig = s.sign(b"msg").unwrap();
assert_eq!(sig.len(), 64);
}
#[test]
fn typed_signer_key_to_pkcs8_ed25519_roundtrip() {
use ring::rand::SystemRandom;
use ring::signature::Ed25519KeyPair;
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
let encoded = s.to_pkcs8().unwrap();
let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
assert_eq!(reparsed.curve(), CurveType::Ed25519);
assert_eq!(reparsed.public_key(), s.public_key());
assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
}
#[test]
fn typed_signer_key_to_pkcs8_p256_roundtrip() {
let seed = TypedSeed::P256({
let mut scalar = [9u8; 32];
scalar[0] |= 1;
scalar
});
let s = TypedSignerKey::from_seed(seed).unwrap();
let encoded = s.to_pkcs8().unwrap();
let reparsed = TypedSignerKey::from_pkcs8(encoded.as_ref()).unwrap();
assert_eq!(reparsed.curve(), CurveType::P256);
assert_eq!(reparsed.public_key(), s.public_key());
assert_eq!(reparsed.seed.as_bytes(), s.seed.as_bytes());
}
#[test]
fn rotation_signer_alias_still_works() {
use ring::rand::SystemRandom;
use ring::signature::Ed25519KeyPair;
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
let s = TypedSignerKey::from_pkcs8(pkcs8.as_ref()).unwrap();
assert_eq!(s.curve(), CurveType::Ed25519);
}
#[test]
fn typed_signer_key_from_parts_rejects_mismatched_pubkey_length() {
let seed = TypedSeed::Ed25519([1u8; 32]);
let wrong_len_pk = vec![0u8; 33]; let err = TypedSignerKey::from_parts(seed, wrong_len_pk).unwrap_err();
assert!(matches!(err, CryptoError::InvalidPrivateKey(_)));
}
#[test]
#[cfg(not(feature = "fips"))]
fn sign_p256_is_rfc6979_deterministic() {
let seed = TypedSeed::P256([7u8; 32]);
let msg = b"fn-128.T2 determinism";
let a = sign(&seed, msg).unwrap();
let b = sign(&seed, msg).unwrap();
let c = sign(&seed, msg).unwrap();
assert_eq!(a, b);
assert_eq!(b, c);
assert_eq!(a.len(), 64);
}
#[test]
fn sign_ed25519_is_deterministic() {
let seed = TypedSeed::Ed25519([11u8; 32]);
let msg = b"fn-128.T2 determinism";
let a = sign(&seed, msg).unwrap();
let b = sign(&seed, msg).unwrap();
assert_eq!(a, b);
assert_eq!(a.len(), 64);
}
#[tokio::test]
#[cfg(not(feature = "fips"))]
async fn sync_sign_matches_async_sign_typed_p256() {
use crate::provider::CryptoProvider;
use crate::ring_provider::RingCryptoProvider;
let seed = TypedSeed::P256([42u8; 32]);
let msg = b"parity check";
let sync_sig = sign(&seed, msg).unwrap();
let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
assert_eq!(sync_sig, async_sig);
}
#[tokio::test]
async fn sync_sign_matches_async_sign_typed_ed25519() {
use crate::provider::CryptoProvider;
use crate::ring_provider::RingCryptoProvider;
let seed = TypedSeed::Ed25519([19u8; 32]);
let msg = b"parity check";
let sync_sig = sign(&seed, msg).unwrap();
let async_sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
assert_eq!(sync_sig, async_sig);
}
#[tokio::test]
async fn sync_public_key_matches_async_typed_public_key() {
use crate::provider::CryptoProvider;
use crate::ring_provider::RingCryptoProvider;
for (name, seed) in [
("ed25519", TypedSeed::Ed25519([23u8; 32])),
("p256", TypedSeed::P256([29u8; 32])),
] {
let sync_pk = public_key(&seed).unwrap();
let async_pk = RingCryptoProvider
.typed_public_key_from_seed(&seed)
.await
.unwrap();
assert_eq!(sync_pk, async_pk, "pub key drift on {name}");
}
}
#[tokio::test]
async fn sign_typed_and_verify_typed_round_trip() {
use crate::provider::CryptoProvider;
use crate::ring_provider::RingCryptoProvider;
for seed in [TypedSeed::Ed25519([31u8; 32]), TypedSeed::P256([37u8; 32])] {
let msg = b"curve-agnostic round trip";
let pk = RingCryptoProvider
.typed_public_key_from_seed(&seed)
.await
.unwrap();
let sig = RingCryptoProvider.sign_typed(&seed, msg).await.unwrap();
RingCryptoProvider
.verify_typed(seed.curve(), &pk, msg, &sig)
.await
.expect("verify_typed should accept matching signature");
}
}
}
}