use crate::identity::AgentIdentity;
use crate::muragent::MuragentError;
use ed25519_dalek::{Signature, Signer, VerifyingKey};
use serde::{Deserialize, Serialize};
pub fn pae(payload_type: &str, payload: &str) -> Vec<u8> {
let mut out = b"DSSEv1 ".to_vec();
out.extend_from_slice(payload_type.len().to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(payload_type.as_bytes());
out.push(b' ');
out.extend_from_slice(payload.len().to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(payload.as_bytes());
out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DsseEnvelope {
#[serde(rename = "payloadType")]
pub payload_type: String,
pub payload: String,
pub signatures: Vec<DsseSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DsseSignature {
pub keyid: String,
#[serde(rename = "publicKey")]
pub public_key: String,
pub sig: String,
}
pub fn sign(
payload_type: &str,
payload_json: &str,
identity: &AgentIdentity,
) -> Result<DsseEnvelope, MuragentError> {
use base64::{Engine, engine::general_purpose::STANDARD as B64};
let pae_bytes = pae(payload_type, payload_json);
let signing_key = identity.signing_key();
let signature: Signature = signing_key.sign(&pae_bytes);
let verifying_key = signing_key.verifying_key();
let pubkey_bytes = verifying_key.as_bytes();
let keyid = keyid_from_pubkey(pubkey_bytes);
let envelope = DsseEnvelope {
payload_type: payload_type.to_string(),
payload: B64.encode(payload_json.as_bytes()),
signatures: vec![DsseSignature {
keyid,
public_key: B64.encode(pubkey_bytes),
sig: B64.encode(signature.to_bytes()),
}],
};
Ok(envelope)
}
pub fn verify(envelope: &DsseEnvelope, expected_payload_type: &str) -> Result<(), MuragentError> {
use base64::{Engine, engine::general_purpose::STANDARD as B64};
if envelope.payload_type != expected_payload_type {
return Err(MuragentError::DsseError(format!(
"payload type mismatch: expected '{}', got '{}'",
expected_payload_type, envelope.payload_type
)));
}
let payload_bytes = B64
.decode(&envelope.payload)
.map_err(|e| MuragentError::DsseError(format!("payload base64: {e}")))?;
let payload_str = String::from_utf8(payload_bytes)
.map_err(|e| MuragentError::DsseError(format!("payload utf-8: {e}")))?;
if envelope.signatures.is_empty() {
return Err(MuragentError::DsseError("no signatures in envelope".into()));
}
let sig_entry = &envelope.signatures[0];
let pae_bytes = pae(expected_payload_type, &payload_str);
let pubkey_bytes = B64
.decode(&sig_entry.public_key)
.map_err(|e| MuragentError::DsseError(format!("public_key base64: {e}")))?;
let pubkey_arr: [u8; 32] = pubkey_bytes
.as_slice()
.try_into()
.map_err(|_| MuragentError::DsseError("public_key not 32 bytes".into()))?;
let verifying_key = VerifyingKey::from_bytes(&pubkey_arr)
.map_err(|e| MuragentError::DsseError(format!("pubkey decode: {e}")))?;
let sig_bytes = B64
.decode(&sig_entry.sig)
.map_err(|e| MuragentError::DsseError(format!("sig base64: {e}")))?;
let sig_arr: [u8; 64] = sig_bytes
.as_slice()
.try_into()
.map_err(|_| MuragentError::DsseError("sig not 64 bytes".into()))?;
let signature = Signature::from_bytes(&sig_arr);
verifying_key
.verify_strict(&pae_bytes, &signature)
.map_err(|e| MuragentError::InvalidSignature(format!("Ed25519 verify_strict: {e}")))?;
Ok(())
}
fn keyid_from_pubkey(pubkey: &[u8; 32]) -> String {
use sha2::Digest;
let hash = sha2::Sha256::digest(pubkey);
let hex = format!("{:x}", hash);
format!("ed25519-{}", &hex[..8])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::AgentIdentity;
#[test]
fn pae_is_deterministic() {
let a = pae("application/vnd.in-toto+json", r#"{"a":1}"#);
let b = pae("application/vnd.in-toto+json", r#"{"a":1}"#);
assert_eq!(a, b);
}
#[test]
fn pae_byte_lengths_not_char_counts() {
let pae_bytes = pae("type", "café");
let pae_str = String::from_utf8(pae_bytes).unwrap();
assert!(
pae_str.contains(" 5 café"),
"payload length should be 5 bytes, got: {pae_str}"
);
}
#[test]
fn sign_and_verify_roundtrip() {
let identity = AgentIdentity::generate();
let payload = r#"{"manifest_sha256":"abc123"}"#;
let envelope = sign("application/vnd.in-toto+json", payload, &identity).unwrap();
verify(&envelope, "application/vnd.in-toto+json").unwrap();
}
#[test]
fn verify_rejects_wrong_payload_type() {
let identity = AgentIdentity::generate();
let envelope = sign("application/vnd.in-toto+json", "{}", &identity).unwrap();
assert!(verify(&envelope, "wrong/type").is_err());
}
#[test]
fn verify_rejects_tampered_payload() {
let identity = AgentIdentity::generate();
let mut envelope = sign("application/vnd.in-toto+json", r#"{"a":1}"#, &identity).unwrap();
use base64::{Engine, engine::general_purpose::STANDARD as B64};
envelope.payload = B64.encode(r#"{"a":2}"#);
assert!(verify(&envelope, "application/vnd.in-toto+json").is_err());
}
#[test]
fn verify_rejects_empty_signatures() {
use base64::{Engine, engine::general_purpose::STANDARD as B64};
let envelope = DsseEnvelope {
payload_type: "application/vnd.in-toto+json".into(),
payload: B64.encode("{}"),
signatures: vec![],
};
assert!(verify(&envelope, "application/vnd.in-toto+json").is_err());
}
}