prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use alloc::string::{String, ToString};
use alloc::vec::Vec;

use crate::crypto::{sha256, Ed25519Signature};
use crate::message_pack::{
    decode_owned, encode_owned, MessagePackDecodeLimits, MessagePackOwnedError, MessagePackValue,
};

use super::{IdentityHash, PrivateIdentityMaterial, PublicIdentityMaterial};

pub const SIGNED_ARTIFACT_SIGNATURE_LEN: usize = Ed25519Signature::LEN;

const SIGNED_ARTIFACT_MAXIMUM_LENGTH: usize = 16 * 1024 * 1024;

#[derive(Debug, Clone, PartialEq)]
pub struct ValidatedSignedArtifact {
    pub signer: PublicIdentityMaterial,
    pub metadata: Vec<(String, MessagePackValue)>,
    pub embedded_message: Option<Vec<u8>>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignedArtifactError {
    TooShort,
    TooLarge,
    MessagePack(MessagePackOwnedError),
    InvalidEnvelope,
    UnsupportedHashType,
    InvalidPublicKey,
    InvalidSigner,
    UnexpectedSigner,
    MessageRequired,
    InvalidMessageHash,
    InvalidSignature,
}

pub fn create_signed_artifact(
    signer: &PrivateIdentityMaterial,
    message: &[u8],
    embed_message: bool,
    metadata: &[(String, MessagePackValue)],
) -> Result<Vec<u8>, SignedArtifactError> {
    let metadata_length = metadata
        .iter()
        .filter(|(key, _)| key != "signer" && key != "pubkey")
        .count()
        .checked_add(2)
        .ok_or(SignedArtifactError::TooLarge)?;
    let mut metadata_entries = Vec::with_capacity(metadata_length);
    metadata_entries.push((
        MessagePackValue::String("signer".to_string()),
        MessagePackValue::Binary(signer.identity_hash().as_bytes().to_vec()),
    ));
    metadata_entries.push((
        MessagePackValue::String("pubkey".to_string()),
        MessagePackValue::Binary(signer.public().as_bytes().to_vec()),
    ));
    for (key, value) in metadata {
        if key != "signer" && key != "pubkey" {
            metadata_entries.push((MessagePackValue::String(key.clone()), value.clone()));
        }
    }
    let mut envelope_entries = Vec::with_capacity(if embed_message { 4 } else { 3 });
    envelope_entries.push((
        MessagePackValue::String("hashtype".to_string()),
        MessagePackValue::String("sha256".to_string()),
    ));
    envelope_entries.push((
        MessagePackValue::String("hash".to_string()),
        MessagePackValue::Binary(sha256(message).to_vec()),
    ));
    envelope_entries.push((
        MessagePackValue::String("meta".to_string()),
        MessagePackValue::Map(metadata_entries),
    ));
    if embed_message {
        envelope_entries.push((
            MessagePackValue::String("message".to_string()),
            MessagePackValue::Binary(message.to_vec()),
        ));
    }
    let envelope = encode_owned(&MessagePackValue::Map(envelope_entries))
        .map_err(SignedArtifactError::MessagePack)?;
    let artifact_length = envelope
        .len()
        .checked_add(SIGNED_ARTIFACT_SIGNATURE_LEN)
        .ok_or(SignedArtifactError::TooLarge)?;
    if artifact_length > SIGNED_ARTIFACT_MAXIMUM_LENGTH {
        return Err(SignedArtifactError::TooLarge);
    }
    let signature = signer.sign(&envelope);
    let mut artifact = Vec::with_capacity(artifact_length);
    artifact.extend_from_slice(&signature.0);
    artifact.extend_from_slice(&envelope);
    Ok(artifact)
}

pub fn validate_signed_artifact(
    artifact: &[u8],
    message: Option<&[u8]>,
    required_signer: Option<IdentityHash>,
) -> Result<ValidatedSignedArtifact, SignedArtifactError> {
    if artifact.len() <= SIGNED_ARTIFACT_SIGNATURE_LEN {
        return Err(SignedArtifactError::TooShort);
    }
    if artifact.len() > SIGNED_ARTIFACT_MAXIMUM_LENGTH {
        return Err(SignedArtifactError::TooLarge);
    }
    let (signature, envelope) = artifact.split_at(SIGNED_ARTIFACT_SIGNATURE_LEN);
    let signature: [u8; SIGNED_ARTIFACT_SIGNATURE_LEN] = signature
        .try_into()
        .map_err(|_| SignedArtifactError::TooShort)?;
    let decoded = decode_owned(envelope, MessagePackDecodeLimits::default())
        .map_err(SignedArtifactError::MessagePack)?;
    let entries = map(&decoded)?;
    let hash_type = string(field(entries, "hashtype")?)?;
    if hash_type != "sha256" {
        return Err(SignedArtifactError::UnsupportedHashType);
    }
    let expected_hash = binary(field(entries, "hash")?)?;
    let metadata_entries = map(field(entries, "meta")?)?;
    let signer_hash = binary(field(metadata_entries, "signer")?)?;
    let public_key = binary(field(metadata_entries, "pubkey")?)?;
    let signer = PublicIdentityMaterial::from_slice(public_key)
        .map_err(|_| SignedArtifactError::InvalidPublicKey)?;
    if signer.identity_hash().as_bytes().as_slice() != signer_hash {
        return Err(SignedArtifactError::InvalidSigner);
    }
    if required_signer.is_some_and(|required| required != signer.identity_hash()) {
        return Err(SignedArtifactError::UnexpectedSigner);
    }
    let embedded_message = optional_field(entries, "message")
        .map(binary)
        .transpose()?
        .map(<[u8]>::to_vec);
    let message = message
        .or(embedded_message.as_deref())
        .ok_or(SignedArtifactError::MessageRequired)?;
    if sha256(message).as_slice() != expected_hash {
        return Err(SignedArtifactError::InvalidMessageHash);
    }
    signer
        .verify(envelope, &Ed25519Signature(signature))
        .map_err(|_| SignedArtifactError::InvalidSignature)?;
    let mut metadata = Vec::with_capacity(metadata_entries.len().saturating_sub(2));
    for (key, value) in metadata_entries {
        let MessagePackValue::String(key) = key else {
            return Err(SignedArtifactError::InvalidEnvelope);
        };
        if key != "signer" && key != "pubkey" {
            metadata.push((key.clone(), value.clone()));
        }
    }
    Ok(ValidatedSignedArtifact {
        signer,
        metadata,
        embedded_message,
    })
}

fn map(
    value: &MessagePackValue,
) -> Result<&[(MessagePackValue, MessagePackValue)], SignedArtifactError> {
    match value {
        MessagePackValue::Map(entries) => Ok(entries),
        _ => Err(SignedArtifactError::InvalidEnvelope),
    }
}

fn field<'a>(
    entries: &'a [(MessagePackValue, MessagePackValue)],
    name: &str,
) -> Result<&'a MessagePackValue, SignedArtifactError> {
    optional_field(entries, name).ok_or(SignedArtifactError::InvalidEnvelope)
}

fn optional_field<'a>(
    entries: &'a [(MessagePackValue, MessagePackValue)],
    name: &str,
) -> Option<&'a MessagePackValue> {
    entries.iter().find_map(|(key, value)| match key {
        MessagePackValue::String(key) if key == name => Some(value),
        _ => None,
    })
}

fn string(value: &MessagePackValue) -> Result<&str, SignedArtifactError> {
    match value {
        MessagePackValue::String(value) => Ok(value),
        _ => Err(SignedArtifactError::InvalidEnvelope),
    }
}

fn binary(value: &MessagePackValue) -> Result<&[u8], SignedArtifactError> {
    match value {
        MessagePackValue::Binary(value) => Ok(value),
        _ => Err(SignedArtifactError::InvalidEnvelope),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const RNS_RSG: &str = "e44d954d391c2393bdd24ebcfb94ba12db4ea2fc6c0f37b34b4072d10657655a521037c83b09f96a894640e7d5d9796022be27f7e177201a2185a298b844e30283a86861736874797065a6736861323536a468617368c4200e231b72dd5437d4095002c7d07b34ff13571911857c83b1281e576cee65fa4ea46d65746182a67369676e6572c4104cd0cc45a7405dbd5cf9b5be1ef92f10a67075626b6579c4400faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737";
    const RNS_RSM: &str = "559d62a364ad9f2968dd9c659434de9094dadd01e810ebb1dcbedcfdc184985e7cd125ed350242da97e9e446a2f59322b938d436c1cd48e6579923cd95f1ff0184a86861736874797065a6736861323536a468617368c42048a2c05ff78d6f7880eff7696a3e4a352131a2b3e3d9c2abc2d8b0aef09e8527a46d65746186a67369676e6572c4104cd0cc45a7405dbd5cf9b5be1ef92f10a67075626b6579c4400faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737a46e616d65a450726e73a776657273696f6e03a47461677392a36f6e65a374776fa6737461626c65c3a76d657373616765c40e6d6573736167652d6f7261636c65";

    fn fixed_private() -> PrivateIdentityMaterial {
        let mut bytes = [0u8; 64];
        bytes[..32].fill(0x22);
        bytes[32..].fill(0x11);
        PrivateIdentityMaterial::from_bytes(bytes)
    }

    fn from_hex(value: &str) -> Vec<u8> {
        value
            .as_bytes()
            .chunks_exact(2)
            .map(|pair| {
                let digits = core::str::from_utf8(pair).unwrap();
                u8::from_str_radix(digits, 16).unwrap()
            })
            .collect()
    }

    #[test]
    fn creates_the_rns_1_4_2_canonical_rsg() {
        let artifact =
            create_signed_artifact(&fixed_private(), b"artifact-oracle", false, &[]).unwrap();
        assert_eq!(artifact, from_hex(RNS_RSG));
    }

    #[test]
    fn creates_and_validates_the_rns_1_4_2_canonical_rsm() {
        let metadata = vec![
            (
                "name".to_string(),
                MessagePackValue::String("Prns".to_string()),
            ),
            ("version".to_string(), MessagePackValue::Unsigned(3)),
            (
                "tags".to_string(),
                MessagePackValue::Array(vec![
                    MessagePackValue::String("one".to_string()),
                    MessagePackValue::String("two".to_string()),
                ]),
            ),
            ("stable".to_string(), MessagePackValue::Boolean(true)),
        ];
        let artifact =
            create_signed_artifact(&fixed_private(), b"message-oracle", true, &metadata).unwrap();
        assert_eq!(artifact, from_hex(RNS_RSM));
        let validated = validate_signed_artifact(&artifact, None, None).unwrap();
        assert_eq!(
            validated.embedded_message.as_deref(),
            Some(b"message-oracle".as_slice())
        );
        assert_eq!(validated.metadata, metadata);
    }

    #[test]
    fn validates_an_rns_artifact_and_rejects_wrong_message_or_signer() {
        let artifact = from_hex(RNS_RSG);
        let validated =
            validate_signed_artifact(&artifact, Some(b"artifact-oracle"), None).unwrap();
        assert_eq!(
            validated.signer.identity_hash(),
            fixed_private().identity_hash()
        );
        assert_eq!(
            validate_signed_artifact(&artifact, Some(b"wrong"), None),
            Err(SignedArtifactError::InvalidMessageHash)
        );
        assert_eq!(
            validate_signed_artifact(
                &artifact,
                Some(b"artifact-oracle"),
                Some(IdentityHash::new([0x55; 16]))
            ),
            Err(SignedArtifactError::UnexpectedSigner)
        );
    }
}