use async_trait::async_trait;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SigningAlgorithm {
EcdsaSha256P256,
EcdsaSha384P384,
}
impl SigningAlgorithm {
pub fn as_str(self) -> &'static str {
match self {
Self::EcdsaSha256P256 => "ecdsa-sha256-p256",
Self::EcdsaSha384P384 => "ecdsa-sha384-p384",
}
}
}
#[async_trait]
pub trait PlatformSigner: Send + Sync {
fn key_id(&self) -> &str;
fn algorithm(&self) -> SigningAlgorithm;
async fn public_key_der(&self) -> Result<Vec<u8>, SignerError>;
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError>;
}
#[derive(Debug, Error)]
pub enum SignerError {
#[error("signer backend error: {0}")]
Backend(String),
#[error("invalid key id: {0}")]
InvalidKeyId(String),
#[error("missing environment variable: {0}")]
MissingEnv(&'static str),
#[error("unexpected public-key encoding from backend: {0}")]
UnexpectedPublicKey(String),
}
pub struct MockSigner {
key_id: String,
keypair: ring::signature::EcdsaKeyPair,
public_key_der: Vec<u8>,
algorithm: SigningAlgorithm,
}
impl std::fmt::Debug for MockSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MockSigner")
.field("key_id", &self.key_id)
.field("algorithm", &self.algorithm)
.finish_non_exhaustive()
}
}
impl MockSigner {
pub fn generate(key_id: impl Into<String>) -> Result<Self, SignerError> {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
&rng,
)
.map_err(|e| SignerError::Backend(format!("ring pkcs8 generate failed: {e}")))?;
let keypair = ring::signature::EcdsaKeyPair::from_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
pkcs8.as_ref(),
&rng,
)
.map_err(|e| SignerError::Backend(format!("ring keypair load failed: {e}")))?;
let raw_pub = ring::signature::KeyPair::public_key(&keypair).as_ref().to_vec();
let public_key_der = wrap_p256_spki(&raw_pub);
Ok(Self {
key_id: key_id.into(),
keypair,
public_key_der,
algorithm: SigningAlgorithm::EcdsaSha256P256,
})
}
}
#[async_trait]
impl PlatformSigner for MockSigner {
fn key_id(&self) -> &str {
&self.key_id
}
fn algorithm(&self) -> SigningAlgorithm {
self.algorithm
}
async fn public_key_der(&self) -> Result<Vec<u8>, SignerError> {
Ok(self.public_key_der.clone())
}
async fn sign(&self, message: &[u8]) -> Result<Vec<u8>, SignerError> {
let rng = ring::rand::SystemRandom::new();
let sig = self
.keypair
.sign(&rng, message)
.map_err(|e| SignerError::Backend(format!("ring sign failed: {e}")))?;
Ok(sig.as_ref().to_vec())
}
}
fn wrap_p256_spki(raw_uncompressed_point: &[u8]) -> Vec<u8> {
const ALG_PREFIX: &[u8] = &[
0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, ];
let bitstring_len = 1 + raw_uncompressed_point.len(); let mut bitstring = Vec::with_capacity(2 + bitstring_len);
bitstring.push(0x03); bitstring.push(bitstring_len as u8);
bitstring.push(0x00); bitstring.extend_from_slice(raw_uncompressed_point);
let body_len = ALG_PREFIX.len() + bitstring.len();
let mut out = Vec::with_capacity(2 + body_len);
out.push(0x30); out.push(body_len as u8);
out.extend_from_slice(ALG_PREFIX);
out.extend_from_slice(&bitstring);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn mock_signer_round_trips() {
let signer = MockSigner::generate("test-key-1").unwrap();
assert_eq!(signer.key_id(), "test-key-1");
assert_eq!(signer.algorithm(), SigningAlgorithm::EcdsaSha256P256);
let msg = b"hello mockforge";
let sig = signer.sign(msg).await.unwrap();
let pub_der = signer.public_key_der().await.unwrap();
let raw_point = extract_p256_point_from_spki(&pub_der).expect("valid spki");
let pubkey = ring::signature::UnparsedPublicKey::new(
&ring::signature::ECDSA_P256_SHA256_ASN1,
&raw_point,
);
pubkey.verify(msg, &sig).expect("signature should verify");
}
#[tokio::test]
async fn mock_signer_rejects_tampered_message() {
let signer = MockSigner::generate("test-key-2").unwrap();
let sig = signer.sign(b"original").await.unwrap();
let pub_der = signer.public_key_der().await.unwrap();
let raw_point = extract_p256_point_from_spki(&pub_der).unwrap();
let pubkey = ring::signature::UnparsedPublicKey::new(
&ring::signature::ECDSA_P256_SHA256_ASN1,
&raw_point,
);
assert!(pubkey.verify(b"tampered", &sig).is_err());
}
#[test]
fn signing_algorithm_wire_form_is_stable() {
assert_eq!(SigningAlgorithm::EcdsaSha256P256.as_str(), "ecdsa-sha256-p256");
assert_eq!(SigningAlgorithm::EcdsaSha384P384.as_str(), "ecdsa-sha384-p384");
}
fn extract_p256_point_from_spki(spki: &[u8]) -> Option<Vec<u8>> {
const HEADER_LEN: usize = 26; if spki.len() <= HEADER_LEN {
return None;
}
Some(spki[HEADER_LEN..].to_vec())
}
}