use crate::CryptoError;
use aitp_core::{Aid, AidAlgorithm, ED25519_SIGNATURE_BASE64URL_LEN};
use base64ct::{Base64UrlUnpadded, Encoding};
use ed25519_dalek::{
Signature as DalekSignature, Signer as Ed25519Signer, SigningKey as DalekSigningKey,
VerifyingKey as DalekVerifyingKey,
};
use p256::ecdsa::{
signature::{Signer as P256Signer, Verifier as P256Verifier},
Signature as P256Signature, SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey,
};
pub enum AitpSigningKey {
Ed25519 {
inner: DalekSigningKey,
aid: Aid,
},
P256 {
inner: P256SigningKey,
aid: Aid,
},
}
impl AitpSigningKey {
pub fn generate() -> Self {
Self::generate_ed25519()
}
pub fn generate_ed25519() -> Self {
let inner = DalekSigningKey::generate(&mut rand::rngs::OsRng);
let aid = Aid::from_ed25519(&inner.verifying_key().to_bytes());
Self::Ed25519 { inner, aid }
}
pub fn generate_p256() -> Self {
let inner = P256SigningKey::random(&mut rand::rngs::OsRng);
let aid = Self::p256_aid_for(&inner);
Self::P256 { inner, aid }
}
pub fn from_seed(seed: &[u8; 32]) -> Self {
Self::from_ed25519_seed(seed)
}
pub fn from_ed25519_seed(seed: &[u8; 32]) -> Self {
let inner = DalekSigningKey::from_bytes(seed);
let aid = Aid::from_ed25519(&inner.verifying_key().to_bytes());
Self::Ed25519 { inner, aid }
}
pub fn from_p256_seed(seed: &[u8; 32]) -> Result<Self, CryptoError> {
let inner = P256SigningKey::from_bytes(seed.into())
.map_err(|e| CryptoError::KeyParseFailed(e.to_string()))?;
let aid = Self::p256_aid_for(&inner);
Ok(Self::P256 { inner, aid })
}
pub fn aid(&self) -> &Aid {
match self {
Self::Ed25519 { aid, .. } => aid,
Self::P256 { aid, .. } => aid,
}
}
pub fn verifying_key(&self) -> AitpVerifyingKey {
match self {
Self::Ed25519 { inner, .. } => AitpVerifyingKey::Ed25519(inner.verifying_key()),
Self::P256 { inner, .. } => AitpVerifyingKey::P256(*inner.verifying_key()),
}
}
pub fn algorithm(&self) -> AidAlgorithm {
match self {
Self::Ed25519 { .. } => AidAlgorithm::Ed25519,
Self::P256 { .. } => AidAlgorithm::P256,
}
}
pub fn sign(&self, message: &[u8]) -> Signature {
match self {
Self::Ed25519 { inner, .. } => {
let sig = <DalekSigningKey as Ed25519Signer<DalekSignature>>::sign(inner, message);
Signature(Base64UrlUnpadded::encode_string(&sig.to_bytes()))
}
Self::P256 { inner, .. } => {
let sig: P256Signature =
<P256SigningKey as P256Signer<P256Signature>>::sign(inner, message);
let encoded = Base64UrlUnpadded::encode_string(&sig.to_bytes());
Signature(format!("p256.{encoded}"))
}
}
}
pub(crate) fn sign_raw(&self, message: &[u8]) -> [u8; 64] {
match self {
Self::Ed25519 { inner, .. } => {
let sig = <DalekSigningKey as Ed25519Signer<DalekSignature>>::sign(inner, message);
sig.to_bytes()
}
Self::P256 { inner, .. } => {
let sig: P256Signature =
<P256SigningKey as P256Signer<P256Signature>>::sign(inner, message);
sig.to_bytes().into()
}
}
}
fn p256_aid_for(inner: &P256SigningKey) -> Aid {
let encoded = inner.verifying_key().to_encoded_point(true);
let bytes = encoded.as_bytes();
debug_assert_eq!(bytes.len(), 33, "P-256 SEC1-compressed must be 33 bytes");
let mut arr = [0u8; 33];
arr.copy_from_slice(bytes);
Aid::from_p256(&arr)
}
}
impl std::fmt::Debug for AitpSigningKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AitpSigningKey")
.field("algorithm", &self.algorithm())
.field("aid", &self.aid())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub enum AitpVerifyingKey {
Ed25519(DalekVerifyingKey),
P256(P256VerifyingKey),
}
impl AitpVerifyingKey {
pub fn from_aid(aid: &Aid) -> Result<Self, CryptoError> {
match aid.algorithm() {
AidAlgorithm::Ed25519 => {
let bytes = aid.to_ed25519_bytes();
DalekVerifyingKey::from_bytes(&bytes)
.map(Self::Ed25519)
.map_err(|e| CryptoError::AidNotEd25519(e.to_string()))
}
AidAlgorithm::P256 => {
let bytes = aid.to_p256_bytes();
P256VerifyingKey::from_sec1_bytes(&bytes)
.map(Self::P256)
.map_err(|e| CryptoError::KeyParseFailed(e.to_string()))
}
other => Err(CryptoError::KeyParseFailed(format!(
"AID algorithm {other:?} not supported by this AitpVerifyingKey build"
))),
}
}
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, CryptoError> {
DalekVerifyingKey::from_bytes(bytes)
.map(Self::Ed25519)
.map_err(|e| CryptoError::KeyParseFailed(e.to_string()))
}
pub fn from_compressed(bytes: &[u8]) -> Result<Self, CryptoError> {
match bytes.len() {
32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(bytes);
Self::from_bytes(&arr)
}
33 => P256VerifyingKey::from_sec1_bytes(bytes)
.map(Self::P256)
.map_err(|e| CryptoError::KeyParseFailed(e.to_string())),
other => Err(CryptoError::KeyParseFailed(format!(
"unsupported compressed pubkey length: {other} (expected 32 for Ed25519 or 33 for P-256 SEC1-compressed)",
))),
}
}
pub fn verify(&self, message: &[u8], sig: &Signature) -> Result<(), CryptoError> {
match (self, sig.algorithm()) {
(Self::Ed25519(vk), SignatureAlgorithm::Ed25519) => {
let raw = Base64UrlUnpadded::decode_vec(sig.payload())
.map_err(|_| CryptoError::SignatureInvalid)?;
if raw.len() != 64 {
return Err(CryptoError::SignatureInvalid);
}
let mut buf = [0u8; 64];
buf.copy_from_slice(&raw);
let dalek_sig = DalekSignature::from_bytes(&buf);
vk.verify_strict(message, &dalek_sig)
.map_err(|_| CryptoError::SignatureInvalid)
}
(Self::P256(vk), SignatureAlgorithm::P256) => {
let raw = Base64UrlUnpadded::decode_vec(sig.payload())
.map_err(|_| CryptoError::SignatureInvalid)?;
if raw.len() != 64 {
return Err(CryptoError::SignatureInvalid);
}
let p256_sig =
P256Signature::from_slice(&raw).map_err(|_| CryptoError::SignatureInvalid)?;
vk.verify(message, &p256_sig)
.map_err(|_| CryptoError::SignatureInvalid)
}
_ => Err(CryptoError::SignatureInvalid),
}
}
pub(crate) fn verify_raw(&self, message: &[u8], sig: &[u8]) -> Result<(), CryptoError> {
if sig.len() != 64 {
return Err(CryptoError::SignatureInvalid);
}
match self {
Self::Ed25519(vk) => {
let mut buf = [0u8; 64];
buf.copy_from_slice(sig);
let dalek_sig = DalekSignature::from_bytes(&buf);
vk.verify_strict(message, &dalek_sig)
.map_err(|_| CryptoError::SignatureInvalid)
}
Self::P256(vk) => {
let p256_sig =
P256Signature::from_slice(sig).map_err(|_| CryptoError::SignatureInvalid)?;
vk.verify(message, &p256_sig)
.map_err(|_| CryptoError::SignatureInvalid)
}
}
}
pub fn to_jwk_thumbprint(&self) -> Result<String, CryptoError> {
match self {
Self::Ed25519(vk) => Ok(crate::thumbprint::compute_jwk_thumbprint(&vk.to_bytes())),
Self::P256(vk) => {
let pt = vk.to_encoded_point(false);
let bytes = pt.as_bytes();
if bytes.len() != 65 || bytes[0] != 0x04 {
return Err(CryptoError::KeyParseFailed(format!(
"P-256 verifying key did not encode to SEC1 uncompressed form (len={}, tag={:#x})",
bytes.len(),
bytes.first().copied().unwrap_or(0),
)));
}
let mut x = [0u8; 32];
let mut y = [0u8; 32];
x.copy_from_slice(&bytes[1..33]);
y.copy_from_slice(&bytes[33..65]);
Ok(crate::thumbprint::compute_jwk_thumbprint_p256(&x, &y))
}
}
}
pub fn to_bytes(&self) -> [u8; 32] {
match self {
Self::Ed25519(vk) => vk.to_bytes(),
Self::P256(_) => {
panic!("AitpVerifyingKey::to_bytes called on P-256 key; use to_compressed() or try_to_ed25519_bytes()")
}
}
}
pub fn try_to_ed25519_bytes(&self) -> Option<[u8; 32]> {
match self {
Self::Ed25519(vk) => Some(vk.to_bytes()),
Self::P256(_) => None,
}
}
pub fn to_compressed(&self) -> Vec<u8> {
match self {
Self::Ed25519(vk) => vk.to_bytes().to_vec(),
Self::P256(vk) => vk.to_encoded_point(true).as_bytes().to_vec(),
}
}
pub fn algorithm(&self) -> AidAlgorithm {
match self {
Self::Ed25519(_) => AidAlgorithm::Ed25519,
Self::P256(_) => AidAlgorithm::P256,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SignatureAlgorithm {
Ed25519,
P256,
}
impl Signature {
pub fn parse(s: &str) -> Result<Self, CryptoError> {
if s.contains('=') {
return Err(CryptoError::SignatureMalformed(
"padding is forbidden".into(),
));
}
if let Some(rest) = s.strip_prefix("ed25519.") {
validate_b64url_signature(rest)?;
return Ok(Self(s.to_string()));
}
if let Some(rest) = s.strip_prefix("p256.") {
validate_b64url_signature(rest)?;
return Ok(Self(s.to_string()));
}
validate_b64url_signature(s)?;
Ok(Self(s.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
pub fn algorithm(&self) -> SignatureAlgorithm {
if self.0.starts_with("p256.") {
SignatureAlgorithm::P256
} else {
SignatureAlgorithm::Ed25519
}
}
pub fn payload(&self) -> &str {
if let Some(p) = self.0.strip_prefix("ed25519.") {
p
} else if let Some(p) = self.0.strip_prefix("p256.") {
p
} else {
&self.0
}
}
}
fn validate_b64url_signature(payload: &str) -> Result<(), CryptoError> {
if payload.len() != ED25519_SIGNATURE_BASE64URL_LEN {
return Err(CryptoError::SignatureMalformed(format!(
"expected {} payload characters, got {}",
ED25519_SIGNATURE_BASE64URL_LEN,
payload.len()
)));
}
if !payload.bytes().all(is_base64url_byte) {
return Err(CryptoError::SignatureMalformed(
"non-base64url character".into(),
));
}
Ok(())
}
fn is_base64url_byte(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_')
}
const _: () = {
assert!(ED25519_SIGNATURE_BASE64URL_LEN == 86);
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_seed_yields_deterministic_aid() {
let key = AitpSigningKey::from_seed(&[7u8; 32]);
let again = AitpSigningKey::from_seed(&[7u8; 32]);
assert_eq!(key.aid(), again.aid());
}
#[test]
fn signs_and_verifies_round_trip() {
let key = AitpSigningKey::from_seed(&[1u8; 32]);
let msg = b"hello aitp";
let sig = key.sign(msg);
assert_eq!(sig.as_str().len(), ED25519_SIGNATURE_BASE64URL_LEN);
let vk = key.verifying_key();
vk.verify(msg, &sig).expect("signature should verify");
assert!(vk.verify(b"tampered", &sig).is_err());
}
#[test]
fn verifying_key_from_aid_round_trips() {
let key = AitpSigningKey::from_seed(&[42u8; 32]);
let vk = AitpVerifyingKey::from_aid(key.aid()).unwrap();
assert_eq!(vk.to_bytes(), key.verifying_key().to_bytes());
}
#[test]
fn signature_parse_rejects_padding() {
let s = "A".repeat(85) + "=";
assert!(matches!(
Signature::parse(&s),
Err(CryptoError::SignatureMalformed(_))
));
}
#[test]
fn signature_parse_rejects_wrong_length() {
assert!(matches!(
Signature::parse(&"A".repeat(85)),
Err(CryptoError::SignatureMalformed(_))
));
assert!(matches!(
Signature::parse(&"A".repeat(87)),
Err(CryptoError::SignatureMalformed(_))
));
}
#[test]
fn signature_parse_rejects_invalid_chars() {
let mut s = "A".repeat(85);
s.push('!');
assert!(matches!(
Signature::parse(&s),
Err(CryptoError::SignatureMalformed(_))
));
}
#[test]
fn signature_parse_accepts_tagged_ed25519() {
let payload = "A".repeat(86);
let s = format!("ed25519.{payload}");
let sig = Signature::parse(&s).unwrap();
assert_eq!(sig.algorithm(), SignatureAlgorithm::Ed25519);
assert_eq!(sig.payload(), payload);
assert_eq!(sig.as_str(), s);
}
#[test]
fn signature_parse_accepts_tagged_p256() {
let payload = "B".repeat(86);
let s = format!("p256.{payload}");
let sig = Signature::parse(&s).unwrap();
assert_eq!(sig.algorithm(), SignatureAlgorithm::P256);
assert_eq!(sig.payload(), payload);
}
#[test]
fn signature_parse_rejects_unknown_tag() {
let s = format!("rsa.{}", "A".repeat(86));
assert!(matches!(
Signature::parse(&s),
Err(CryptoError::SignatureMalformed(_))
));
}
#[test]
fn untagged_signature_defaults_to_ed25519() {
let s = "A".repeat(86);
let sig = Signature::parse(&s).unwrap();
assert_eq!(sig.algorithm(), SignatureAlgorithm::Ed25519);
assert_eq!(sig.payload(), &s);
}
#[test]
fn ed25519_verify_rejects_p256_tagged_signature() {
let key = AitpSigningKey::from_seed(&[3u8; 32]);
let s = format!("p256.{}", "A".repeat(86));
let sig = Signature::parse(&s).unwrap();
assert!(key.verifying_key().verify(b"msg", &sig).is_err());
}
#[test]
fn p256_verifier_round_trip() {
use p256::ecdsa::{signature::Signer as _, SigningKey as P256SigningKey};
let signing_key = P256SigningKey::from_bytes(&[7u8; 32].into()).unwrap();
let p256_pk = signing_key.verifying_key();
let pk_compressed = p256_pk.to_encoded_point(true);
let pk_bytes = pk_compressed.as_bytes();
assert_eq!(pk_bytes.len(), 33);
let mut pk_arr = [0u8; 33];
pk_arr.copy_from_slice(pk_bytes);
let aid = aitp_core::Aid::from_p256(&pk_arr);
let verifier = AitpVerifyingKey::from_aid(&aid).expect("P-256 AID parses");
assert_eq!(verifier.algorithm(), AidAlgorithm::P256);
assert_eq!(verifier.to_compressed(), pk_bytes);
let msg = b"aitp p256 round-trip";
let sig: p256::ecdsa::Signature = signing_key.sign(msg);
let sig_bytes = sig.to_bytes();
let sig_b64 = Base64UrlUnpadded::encode_string(&sig_bytes);
let wire = format!("p256.{sig_b64}");
let parsed = Signature::parse(&wire).unwrap();
assert_eq!(parsed.algorithm(), SignatureAlgorithm::P256);
verifier
.verify(msg, &parsed)
.expect("P-256 signature verifies");
assert!(verifier.verify(b"tampered", &parsed).is_err());
}
#[test]
fn p256_signing_key_round_trip() {
let key = AitpSigningKey::generate_p256();
assert_eq!(key.algorithm(), AidAlgorithm::P256);
assert!(matches!(key.aid().algorithm(), AidAlgorithm::P256));
let msg = b"aitp p256 signing key round-trip";
let sig = key.sign(msg);
assert_eq!(sig.algorithm(), SignatureAlgorithm::P256);
assert!(sig.as_str().starts_with("p256."));
let vk = key.verifying_key();
vk.verify(msg, &sig).expect("p256 round-trip verifies");
assert!(vk.verify(b"tampered", &sig).is_err());
let derived = AitpVerifyingKey::from_aid(key.aid()).unwrap();
assert_eq!(derived.to_compressed(), vk.to_compressed());
}
#[test]
fn p256_from_seed_is_deterministic() {
let a = AitpSigningKey::from_p256_seed(&[5u8; 32]).expect("valid p256 seed");
let b = AitpSigningKey::from_p256_seed(&[5u8; 32]).expect("valid p256 seed");
assert_eq!(a.aid(), b.aid());
let msg = b"deterministic";
assert_eq!(a.sign(msg).as_str(), b.sign(msg).as_str());
}
#[test]
fn try_to_ed25519_bytes_returns_some_for_ed25519_and_none_for_p256() {
let ed = AitpSigningKey::from_seed(&[9u8; 32]);
let p256 = AitpSigningKey::generate_p256();
let ed_bytes = ed.verifying_key().try_to_ed25519_bytes();
let p256_bytes = p256.verifying_key().try_to_ed25519_bytes();
assert!(ed_bytes.is_some());
assert_eq!(ed_bytes.unwrap().len(), 32);
assert!(
p256_bytes.is_none(),
"P-256 key must not yield Ed25519-shaped bytes"
);
}
#[test]
fn ed25519_signing_key_produces_untagged_signature() {
let key = AitpSigningKey::generate();
let sig = key.sign(b"compat");
assert!(!sig.as_str().starts_with("ed25519."));
assert!(!sig.as_str().starts_with("p256."));
assert_eq!(sig.as_str().len(), ED25519_SIGNATURE_BASE64URL_LEN);
}
#[test]
fn p256_jwk_thumbprint_round_trip() {
let key = AitpSigningKey::generate_p256();
let from_signer = key.verifying_key().to_jwk_thumbprint().expect("p256 jkt");
let from_aid = AitpVerifyingKey::from_aid(key.aid())
.unwrap()
.to_jwk_thumbprint()
.expect("p256 jkt via aid");
assert_eq!(from_signer, from_aid);
assert_eq!(from_signer.len(), 43);
}
#[test]
fn p256_jwk_thumbprint_matches_thumbprint_module() {
use p256::ecdsa::SigningKey as P256SigningKey;
let signer = P256SigningKey::from_bytes(&[3u8; 32].into()).unwrap();
let pk = signer.verifying_key();
let pt = pk.to_encoded_point(false);
let bytes = pt.as_bytes();
assert_eq!(bytes.len(), 65);
assert_eq!(bytes[0], 0x04);
let mut x = [0u8; 32];
let mut y = [0u8; 32];
x.copy_from_slice(&bytes[1..33]);
y.copy_from_slice(&bytes[33..65]);
let expected = crate::thumbprint::compute_jwk_thumbprint_p256(&x, &y);
let actual = AitpVerifyingKey::P256(*pk).to_jwk_thumbprint().unwrap();
assert_eq!(actual, expected);
}
#[test]
fn p256_and_ed25519_thumbprints_disagree_even_with_identical_seed() {
let seed = [0x9Cu8; 32];
let ed = AitpSigningKey::from_ed25519_seed(&seed);
let p = AitpSigningKey::from_p256_seed(&seed).unwrap();
let ed_t = ed.verifying_key().to_jwk_thumbprint().unwrap();
let p_t = p.verifying_key().to_jwk_thumbprint().unwrap();
assert_ne!(ed_t, p_t);
}
#[test]
fn p256_verify_rejects_ed25519_signature() {
use p256::ecdsa::SigningKey as P256SigningKey;
let signing_key = P256SigningKey::from_bytes(&[9u8; 32].into()).unwrap();
let pk = signing_key.verifying_key();
let pk_compressed = pk.to_encoded_point(true);
let mut pk_arr = [0u8; 33];
pk_arr.copy_from_slice(pk_compressed.as_bytes());
let aid = aitp_core::Aid::from_p256(&pk_arr);
let verifier = AitpVerifyingKey::from_aid(&aid).unwrap();
let ed_key = AitpSigningKey::from_seed(&[1u8; 32]);
let ed_sig = ed_key.sign(b"msg");
assert!(verifier.verify(b"msg", &ed_sig).is_err());
}
}