Skip to main content

aap_protocol/
crypto.rs

1//! AAP cryptographic primitives — Ed25519 + SHA-256.
2//! Uses ed25519-dalek (audited) and sha2.
3
4use base64::{engine::general_purpose::STANDARD as B64, Engine};
5use ed25519_dalek::{Signer, SigningKey, VerifyingKey, Verifier, Signature};
6use rand::rngs::OsRng;
7use sha2::{Digest, Sha256};
8use serde_json::Value;
9
10use crate::errors::{AAPError, Result};
11
12/// An Ed25519 keypair for signing AAP documents.
13pub struct KeyPair {
14    signing: SigningKey,
15}
16
17impl KeyPair {
18    /// Generate a new random Ed25519 keypair.
19    pub fn generate() -> Self {
20        Self { signing: SigningKey::generate(&mut OsRng) }
21    }
22
23    /// Return the public key in AAP wire format: `"ed25519:<base64>"`.
24    pub fn public_key_b64(&self) -> String {
25        format!("ed25519:{}", B64.encode(self.signing.verifying_key().as_bytes()))
26    }
27
28    /// Sign `data`, returning the signature in AAP wire format: `"ed25519:<base64>"`.
29    pub fn sign(&self, data: &[u8]) -> String {
30        let sig: Signature = self.signing.sign(data);
31        format!("ed25519:{}", B64.encode(sig.to_bytes()))
32    }
33}
34
35/// Verify an Ed25519 AAP signature.
36pub fn verify_signature(public_key_b64: &str, data: &[u8], signature_b64: &str) -> Result<()> {
37    let pub_bytes = B64
38        .decode(public_key_b64.trim_start_matches("ed25519:"))
39        .map_err(|e| AAPError::Signature(format!("invalid public key: {e}")))?;
40
41    let sig_bytes = B64
42        .decode(signature_b64.trim_start_matches("ed25519:"))
43        .map_err(|e| AAPError::Signature(format!("invalid signature: {e}")))?;
44
45    let key = VerifyingKey::from_bytes(
46        pub_bytes.as_slice().try_into()
47            .map_err(|_| AAPError::Signature("public key must be 32 bytes".into()))?,
48    ).map_err(|e| AAPError::Signature(format!("invalid key: {e}")))?;
49
50    let sig = Signature::from_bytes(
51        sig_bytes.as_slice().try_into()
52            .map_err(|_| AAPError::Signature("signature must be 64 bytes".into()))?,
53    );
54
55    key.verify(data, &sig)
56        .map_err(|_| AAPError::Signature("signature mismatch".into()))
57}
58
59/// Compute `"sha256:<hex>"` of `data`.
60pub fn sha256_of(data: &[u8]) -> String {
61    let hash = Sha256::digest(data);
62    format!("sha256:{}", hex::encode(hash))
63}
64
65/// Compute the SHA-256 of the canonical JSON of a serde_json Value.
66pub fn hash_entry(v: &Value) -> String {
67    let canonical = serde_json::to_string(v).unwrap_or_default();
68    sha256_of(canonical.as_bytes())
69}
70
71/// Return canonical bytes for signing: sorted-key JSON without the "signature" field.
72pub fn signable(v: &Value) -> Result<Vec<u8>> {
73    let mut map = match v.as_object() {
74        Some(m) => m.clone(),
75        None => return Err(AAPError::Serde(serde_json::from_str::<Value>("").unwrap_err())),
76    };
77    map.remove("signature");
78    // Sort keys for canonical representation
79    let sorted: serde_json::Map<String, Value> = map.into_iter().collect();
80    Ok(serde_json::to_vec(&Value::Object(sorted))?)
81}