use crate::encoding::{base64_decode, base64_encode, hex_decode, hex_encode};
use crate::error::{Error, Result};
const SIG_PREFIX: &str = "— ";
const SIG_SPLIT: &str = "\n\n";
const MAX_SIGNATURES: usize = 100;
pub const HYBRID_SIG_IDENTIFIER: &[u8] = b"\xffmetamorphic.app/composite-mldsa-ed25519/v1";
pub const HYBRID_SIG_CONTEXT: &str = "metamorphic.app/signed-note/v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SignatureType {
Ed25519,
MetamorphicHybrid,
}
impl SignatureType {
#[must_use]
pub fn type_identifier(self) -> &'static [u8] {
match self {
SignatureType::Ed25519 => &[0x01],
SignatureType::MetamorphicHybrid => HYBRID_SIG_IDENTIFIER,
}
}
fn detect(key: &[u8]) -> Result<(SignatureType, usize)> {
if key.first() == Some(&0x01) {
return Ok((SignatureType::Ed25519, 1));
}
if key.starts_with(HYBRID_SIG_IDENTIFIER) {
return Ok((
SignatureType::MetamorphicHybrid,
HYBRID_SIG_IDENTIFIER.len(),
));
}
Err(Error::MalformedNote(format!(
"unsupported signature type (leading byte 0x{:02x})",
key.first().copied().unwrap_or(0)
)))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifierKey {
name: String,
key_id: u32,
sig_type: SignatureType,
public_key: Vec<u8>,
}
impl VerifierKey {
pub fn new_ed25519(name: &str, public_key: &[u8]) -> Result<Self> {
if !is_valid_name(name) {
return Err(Error::MalformedNote(format!("invalid key name: {name:?}")));
}
if public_key.len() != 32 {
return Err(Error::MalformedNote(format!(
"Ed25519 public key must be 32 bytes, got {}",
public_key.len()
)));
}
let key_id = compute_key_id(name, SignatureType::Ed25519.type_identifier(), public_key);
Ok(Self {
name: name.to_string(),
key_id,
sig_type: SignatureType::Ed25519,
public_key: public_key.to_vec(),
})
}
pub fn new_hybrid(name: &str, public_key: &[u8]) -> Result<Self> {
if !is_valid_name(name) {
return Err(Error::MalformedNote(format!("invalid key name: {name:?}")));
}
if public_key.is_empty() {
return Err(Error::MalformedNote(
"hybrid composite public key must be non-empty".into(),
));
}
let key_id = compute_key_id(
name,
SignatureType::MetamorphicHybrid.type_identifier(),
public_key,
);
Ok(Self {
name: name.to_string(),
key_id,
sig_type: SignatureType::MetamorphicHybrid,
public_key: public_key.to_vec(),
})
}
pub fn parse(vkey: &str) -> Result<Self> {
let malformed = || Error::MalformedNote(format!("malformed verifier key: {vkey:?}"));
let (name, rest) = vkey.split_once('+').ok_or_else(malformed)?;
let (hash_hex, key_b64) = rest.split_once('+').ok_or_else(malformed)?;
if hash_hex.len() != 8 {
return Err(malformed());
}
let hash_bytes = hex_decode(hash_hex)?;
let declared_id =
u32::from_be_bytes([hash_bytes[0], hash_bytes[1], hash_bytes[2], hash_bytes[3]]);
let key = base64_decode(key_b64)?;
if key.is_empty() || !is_valid_name(name) {
return Err(malformed());
}
let computed_id = key_hash(name, &key);
if computed_id != declared_id {
return Err(Error::MalformedNote(format!(
"verifier key id mismatch: declared {declared_id:08x}, computed {computed_id:08x}"
)));
}
let (sig_type, id_len) = SignatureType::detect(&key)?;
let public_key = &key[id_len..];
match sig_type {
SignatureType::Ed25519 if public_key.len() != 32 => return Err(malformed()),
SignatureType::MetamorphicHybrid if public_key.is_empty() => return Err(malformed()),
_ => {}
}
Ok(Self {
name: name.to_string(),
key_id: declared_id,
sig_type,
public_key: public_key.to_vec(),
})
}
#[must_use]
pub fn encode(&self) -> String {
let id = self.sig_type.type_identifier();
let mut key = Vec::with_capacity(id.len() + self.public_key.len());
key.extend_from_slice(id);
key.extend_from_slice(&self.public_key);
format!(
"{}+{}+{}",
self.name,
hex_encode(&self.key_id.to_be_bytes()),
base64_encode(&key)
)
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn key_id(&self) -> u32 {
self.key_id
}
#[must_use]
pub fn signature_type(&self) -> SignatureType {
self.sig_type
}
#[must_use]
pub fn public_key(&self) -> &[u8] {
&self.public_key
}
#[must_use]
pub fn hybrid_posture_tag(&self) -> Option<u8> {
match self.sig_type {
SignatureType::MetamorphicHybrid => self.public_key.first().copied(),
SignatureType::Ed25519 => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
name: String,
key_id: u32,
signature: Vec<u8>,
}
impl Signature {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn key_id(&self) -> u32 {
self.key_id
}
#[must_use]
pub fn signature(&self) -> &[u8] {
&self.signature
}
#[must_use]
fn to_base64(&self) -> String {
let mut blob = Vec::with_capacity(4 + self.signature.len());
blob.extend_from_slice(&self.key_id.to_be_bytes());
blob.extend_from_slice(&self.signature);
base64_encode(&blob)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedNote {
text: String,
signatures: Vec<Signature>,
}
impl SignedNote {
pub fn new(text: String, signatures: Vec<Signature>) -> Result<Self> {
if !text.ends_with('\n') {
return Err(Error::MalformedNote("note text must end in newline".into()));
}
Ok(Self { text, signatures })
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn signatures(&self) -> &[Signature] {
&self.signatures
}
pub fn parse(msg: &str) -> Result<Self> {
if msg.bytes().any(|b| b < 0x20 && b != b'\n') {
return Err(Error::MalformedNote(
"note contains a forbidden control character".into(),
));
}
let split = msg
.rfind(SIG_SPLIT)
.ok_or_else(|| Error::MalformedNote("missing blank-line signature separator".into()))?;
let text = &msg[..split + 1];
let sig_block = &msg[split + 2..];
if sig_block.is_empty() || !sig_block.ends_with('\n') {
return Err(Error::MalformedNote(
"signature block is empty or unterminated".into(),
));
}
let mut signatures = Vec::new();
for line in sig_block.lines() {
let body = line.strip_prefix(SIG_PREFIX).ok_or_else(|| {
Error::MalformedNote(format!("signature line missing '— ' prefix: {line:?}"))
})?;
let (name, b64) = body
.split_once(' ')
.ok_or_else(|| Error::MalformedNote("signature line missing space".into()))?;
if !is_valid_name(name) || b64.is_empty() {
return Err(Error::MalformedNote(format!(
"invalid signature line name/blob: {line:?}"
)));
}
let blob = base64_decode(b64)?;
if blob.len() < 5 {
return Err(Error::MalformedNote("signature blob too short".into()));
}
let key_id = u32::from_be_bytes([blob[0], blob[1], blob[2], blob[3]]);
signatures.push(Signature {
name: name.to_string(),
key_id,
signature: blob[4..].to_vec(),
});
if signatures.len() > MAX_SIGNATURES {
return Err(Error::MalformedNote("too many signatures".into()));
}
}
Self::new(text.to_string(), signatures)
}
#[must_use]
pub fn marshal(&self) -> String {
let mut out = String::with_capacity(self.text.len() + 1 + self.signatures.len() * 80);
out.push_str(&self.text);
out.push('\n');
for sig in &self.signatures {
out.push_str(SIG_PREFIX);
out.push_str(&sig.name);
out.push(' ');
out.push_str(&sig.to_base64());
out.push('\n');
}
out
}
pub fn verify<'a>(&'a self, trusted: &[VerifierKey]) -> Result<Vec<&'a Signature>> {
let mut verified = Vec::new();
for sig in &self.signatures {
let Some(key) = trusted
.iter()
.find(|k| k.key_id == sig.key_id && k.name == sig.name)
else {
continue; };
let ok = match key.sig_type {
SignatureType::Ed25519 => {
metamorphic_crypto::ed25519_verify(
&key.public_key,
self.text.as_bytes(),
&sig.signature,
)
.unwrap_or(false)
}
SignatureType::MetamorphicHybrid => {
let sig_b64 = base64_encode(&sig.signature);
let pk_b64 = base64_encode(&key.public_key);
metamorphic_crypto::verify(
self.text.as_bytes(),
HYBRID_SIG_CONTEXT,
&sig_b64,
&pk_b64,
)
.unwrap_or(false)
}
};
if ok {
verified.push(sig);
} else {
return Err(Error::InvalidSignature {
name: sig.name.clone(),
key_id: sig.key_id,
});
}
}
if verified.is_empty() {
return Err(Error::NoTrustedSignature);
}
Ok(verified)
}
}
pub fn sign_ed25519(text: &str, name: &str, seed: &[u8]) -> Result<Signature> {
if !is_valid_name(name) {
return Err(Error::MalformedNote(format!("invalid key name: {name:?}")));
}
let public_key = metamorphic_crypto::ed25519_public_key(seed)
.map_err(|e| Error::MalformedNote(format!("invalid Ed25519 seed: {e}")))?;
let key_id = compute_key_id(name, SignatureType::Ed25519.type_identifier(), &public_key);
let signature = metamorphic_crypto::ed25519_sign(seed, text.as_bytes())
.map_err(|e| Error::MalformedNote(format!("Ed25519 signing failed: {e}")))?;
Ok(Signature {
name: name.to_string(),
key_id,
signature: signature.to_vec(),
})
}
pub fn sign_hybrid(text: &str, name: &str, secret_key_b64: &str) -> Result<Signature> {
if !is_valid_name(name) {
return Err(Error::MalformedNote(format!("invalid key name: {name:?}")));
}
let public_key_b64 = metamorphic_crypto::derive_public_key(secret_key_b64)
.map_err(|e| Error::HybridSignature(format!("invalid hybrid secret key: {e}")))?;
let public_key = base64_decode(&public_key_b64)?;
let key_id = compute_key_id(
name,
SignatureType::MetamorphicHybrid.type_identifier(),
&public_key,
);
let sig_b64 = metamorphic_crypto::sign(text.as_bytes(), HYBRID_SIG_CONTEXT, secret_key_b64)
.map_err(|e| Error::HybridSignature(format!("hybrid signing failed: {e}")))?;
let signature = base64_decode(&sig_b64)?;
Ok(Signature {
name: name.to_string(),
key_id,
signature,
})
}
fn key_hash(name: &str, key: &[u8]) -> u32 {
let mut buf = Vec::with_capacity(name.len() + 1 + key.len());
buf.extend_from_slice(name.as_bytes());
buf.push(0x0A);
buf.extend_from_slice(key);
let digest = metamorphic_crypto::hash::sha256(&buf);
u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]])
}
fn compute_key_id(name: &str, type_id: &[u8], public_key: &[u8]) -> u32 {
let mut key = Vec::with_capacity(type_id.len() + public_key.len());
key.extend_from_slice(type_id);
key.extend_from_slice(public_key);
key_hash(name, &key)
}
fn is_valid_name(name: &str) -> bool {
!name.is_empty() && !name.chars().any(|c| c.is_whitespace() || c == '+')
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
const SPEC_VKEY: &str = "example.com/foo+530d903a+AekyeRrm56hApGFkyQR4ZCbV54Id2LKaANYcrnKv3U2k";
const SPEC_NOTE: &str = "This is an example message.\n\n— example.com/foo Uw2QOkn8srV1yJGh2VYRlL1Tnagv1YEq6TfXppzi2ONncAlTgK7Ztg1ERYNZXsYjOBH3mFXmRKuwHjG1Yu72IneyaQM=\n";
#[test]
fn spec_vkey_parses_and_round_trips() {
let vkey = VerifierKey::parse(SPEC_VKEY).unwrap();
assert_eq!(vkey.name(), "example.com/foo");
assert_eq!(vkey.key_id(), 0x530d_903a);
assert_eq!(vkey.signature_type(), SignatureType::Ed25519);
assert_eq!(vkey.encode(), SPEC_VKEY);
}
#[test]
fn spec_note_parses_and_verifies() {
let vkey = VerifierKey::parse(SPEC_VKEY).unwrap();
let note = SignedNote::parse(SPEC_NOTE).unwrap();
assert_eq!(note.text(), "This is an example message.\n");
assert_eq!(note.signatures().len(), 1);
assert_eq!(note.signatures()[0].key_id(), 0x530d_903a);
let verified = note.verify(&[vkey]).unwrap();
assert_eq!(verified.len(), 1);
assert_eq!(note.marshal(), SPEC_NOTE);
}
#[test]
fn tampered_text_fails_verification() {
let vkey = VerifierKey::parse(SPEC_VKEY).unwrap();
let tampered = SPEC_NOTE.replace("example message", "EVIL message");
let note = SignedNote::parse(&tampered).unwrap();
assert!(matches!(
note.verify(&[vkey]),
Err(Error::InvalidSignature { .. })
));
}
#[test]
fn unknown_key_is_ignored_not_trusted() {
let note = SignedNote::parse(SPEC_NOTE).unwrap();
assert!(matches!(note.verify(&[]), Err(Error::NoTrustedSignature)));
}
#[test]
fn sign_and_verify_round_trip() {
let (seed, pk) = metamorphic_crypto::ed25519_generate_keypair();
let text = "origin.example/log\n7\ncm9vdA==\n".to_string();
let sig = sign_ed25519(&text, "origin.example/log", &seed).unwrap();
let note = SignedNote::new(text.clone(), vec![sig]).unwrap();
let vkey = VerifierKey::new_ed25519("origin.example/log", &pk).unwrap();
let verified = note.verify(&[vkey]).unwrap();
assert_eq!(verified.len(), 1);
let reparsed = SignedNote::parse(¬e.marshal()).unwrap();
assert_eq!(reparsed, note);
}
#[test]
fn parse_rejects_control_chars_and_missing_separator() {
assert!(SignedNote::parse("no separator\n").is_err());
assert!(SignedNote::parse("bad\x01char\n\n— a b AAAAAA==\n").is_err());
}
#[test]
fn key_id_matches_spec_formula() {
let vkey = VerifierKey::parse(SPEC_VKEY).unwrap();
let recomputed = compute_key_id(
vkey.name(),
SignatureType::Ed25519.type_identifier(),
&vkey.public_key,
);
assert_eq!(recomputed, 0x530d_903a);
}
#[test]
fn hybrid_type_identifier_uses_0xff_escape() {
let id = SignatureType::MetamorphicHybrid.type_identifier();
assert_eq!(id.first(), Some(&0xff));
assert!(id.len() > 1);
assert_eq!(SignatureType::Ed25519.type_identifier(), &[0x01]);
}
#[test]
fn hybrid_sign_verify_and_vkey_round_trip() {
let kp = metamorphic_crypto::generate_signing_keypair(); let pk = base64_decode(&kp.public_key).unwrap();
let text = "origin.example/log\n7\ncm9vdA==\n".to_string();
let sig = sign_hybrid(&text, "origin.example/log", &kp.secret_key).unwrap();
let note = SignedNote::new(text, vec![sig]).unwrap();
let vkey = VerifierKey::new_hybrid("origin.example/log", &pk).unwrap();
assert_eq!(vkey.signature_type(), SignatureType::MetamorphicHybrid);
assert_eq!(vkey.hybrid_posture_tag(), Some(0x02));
assert_eq!(VerifierKey::parse(&vkey.encode()).unwrap(), vkey);
let verified = note.verify(&[vkey]).unwrap();
assert_eq!(verified.len(), 1);
let reparsed = SignedNote::parse(¬e.marshal()).unwrap();
assert_eq!(reparsed, note);
}
#[test]
fn hybrid_tampered_text_is_rejected() {
let kp = metamorphic_crypto::generate_signing_keypair();
let pk = base64_decode(&kp.public_key).unwrap();
let text = "origin.example/log\n7\ncm9vdA==\n".to_string();
let sig = sign_hybrid(&text, "origin.example/log", &kp.secret_key).unwrap();
let note = SignedNote::new(text, vec![sig]).unwrap();
let forged = SignedNote::new(
"origin.example/log\n8\nZXZpbA==\n".to_string(),
note.signatures().to_vec(),
)
.unwrap();
let vkey = VerifierKey::new_hybrid("origin.example/log", &pk).unwrap();
assert!(matches!(
forged.verify(&[vkey]),
Err(Error::InvalidSignature { .. })
));
}
#[test]
fn classical_and_hybrid_lines_coexist() {
let (seed, ed_pk) = metamorphic_crypto::ed25519_generate_keypair();
let kp = metamorphic_crypto::generate_signing_keypair();
let pk = base64_decode(&kp.public_key).unwrap();
let text = "origin.example/log\n9\ncm9vdA==\n".to_string();
let ed_sig = sign_ed25519(&text, "origin.example/log", &seed).unwrap();
let pq_sig = sign_hybrid(&text, "origin.example/log-pq", &kp.secret_key).unwrap();
let note = SignedNote::new(text, vec![ed_sig, pq_sig]).unwrap();
let ed_vkey = VerifierKey::new_ed25519("origin.example/log", &ed_pk).unwrap();
let pq_vkey = VerifierKey::new_hybrid("origin.example/log-pq", &pk).unwrap();
assert_eq!(
note.verify(std::slice::from_ref(&ed_vkey)).unwrap().len(),
1
);
assert_eq!(note.verify(&[ed_vkey, pq_vkey]).unwrap().len(), 2);
}
}