hns-chat-protocol 0.2.0

Owner-bound HNS Chat identity and opaque mailbox protocol values
Documentation
use hns_chat_protocol::{
    ChatAcknowledgementV1, ChatEnvelopeV1, ChatIdentityBindingV1, ChatKeyMode, ChatProtocolError,
    HNS_CHAT_WIRE_VERSION, MAX_CHAT_ACKNOWLEDGEMENT_SIZE, MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE,
    MAX_CHAT_CIPHERTEXT_SIZE, MAX_CHAT_ENVELOPE_SIZE, MAX_CHAT_EXPIRATION_WINDOW,
    encode_chat_binding, owner_authority_record, parse_chat_binding, select_chat_binding,
    select_chat_binding_from_resource, verify_current_owner_binding,
    xonly_from_compressed_public_key,
};
use hns_covenants::{Covenant, CovenantKind, Resource};
use hns_primitives::Dollarydoos;
use hns_transaction::{Address, Output};
use sha2::{Digest, Sha256};

const VECTORS: &str = include_str!("../fixtures/chat-v1/hns-chat-resource-v1.txt");
const VECTORS_SHA256: &str = include_str!("../fixtures/chat-v1/hns-chat-resource-v1.txt.sha256");

fn vector(name: &str) -> &str {
    VECTORS
        .lines()
        .filter_map(|line| line.split_once('='))
        .find_map(|(key, value)| (key == name).then_some(value))
        .unwrap_or_else(|| panic!("missing release vector {name}"))
}

fn vector_bytes(name: &str) -> Vec<u8> {
    hex::decode(vector(name)).unwrap_or_else(|error| panic!("invalid hex vector {name}: {error}"))
}

fn owner_output(version: u8, program: Vec<u8>) -> Output {
    Output {
        value: Dollarydoos::new(1),
        address: Address::new(version, program).expect("valid fixture owner program"),
        covenant: Covenant {
            kind: CovenantKind::Update,
            items: Vec::new(),
        },
    }
}

#[test]
fn release_source_resource_vectors_cover_canonical_and_rejected_forms() {
    let explicit = parse_chat_binding(vector("valid_explicit")).expect("explicit binding");
    assert_eq!(explicit.generation, 7);
    assert_eq!(
        encode_chat_binding(&explicit).expect("canonical binding"),
        vector("valid_explicit")
    );
    assert_eq!(
        parse_chat_binding(vector("valid_omitted"))
            .expect("compatibility binding")
            .generation,
        1
    );
    assert_eq!(
        parse_chat_binding(vector("valid_max_generation"))
            .expect("maximum generation")
            .generation,
        u32::MAX
    );

    for name in [
        "invalid_version",
        "invalid_key_mode",
        "invalid_missing_key",
        "invalid_uppercase",
        "invalid_duplicate",
        "invalid_unknown",
        "invalid_zero_generation",
        "invalid_empty_generation",
        "invalid_leading_zero_generation",
        "invalid_overflow_generation",
        "invalid_x_coordinate",
    ] {
        assert!(
            parse_chat_binding(vector(name)).is_err(),
            "accepted invalid release vector {name}"
        );
    }
    assert!(parse_chat_binding("hnschat=v1;key=owner;pk=\u{e9}").is_err());
    assert_eq!(
        select_chat_binding(["unrelated"]),
        Err(ChatProtocolError::MissingBinding)
    );
    assert_eq!(
        select_chat_binding([vector("valid_explicit"), vector("valid_omitted")]),
        Err(ChatProtocolError::AmbiguousBinding)
    );

    let binding = vector("valid_explicit").as_bytes();
    let split = binding.len() / 2;
    let mut raw = vec![0, 6, 2, split as u8];
    raw.extend_from_slice(&binding[..split]);
    raw.push((binding.len() - split) as u8);
    raw.extend_from_slice(&binding[split..]);
    let resource = Resource::decode(&raw).expect("chunked TXT resource");
    assert_eq!(
        select_chat_binding_from_resource(&resource).expect("resource binding"),
        explicit
    );

    let mut non_ascii_record = b"hnschat=".to_vec();
    non_ascii_record.push(0xff);
    let mut non_ascii_resource = vec![0, 6, 1, non_ascii_record.len() as u8];
    non_ascii_resource.extend_from_slice(&non_ascii_record);
    let non_ascii = Resource::decode(&non_ascii_resource).expect("opaque TXT bytes");
    assert!(select_chat_binding_from_resource(&non_ascii).is_err());
}

#[test]
fn release_source_owner_vectors_preserve_parity_and_reject_false_authority() {
    let binding = parse_chat_binding(vector("valid_explicit")).expect("binding");
    for (key_name, program_name) in [
        ("owner_even_compressed", "owner_even_program"),
        ("owner_odd_compressed", "owner_odd_program"),
    ] {
        let compressed: [u8; 33] = vector_bytes(key_name)
            .try_into()
            .expect("compressed owner key");
        assert_eq!(
            xonly_from_compressed_public_key(&compressed).expect("x-only owner key"),
            binding.xonly_public_key
        );
        let derived = Address::from_compressed_public_key(&compressed).expect("owner address");
        assert_eq!(hex::encode(&derived.hash), vector(program_name));

        let verified =
            verify_current_owner_binding(&binding, &owner_output(0, vector_bytes(program_name)))
                .expect("current owner binding");
        assert_eq!(verified.original_compressed_public_key(), compressed);
        let authority = owner_authority_record(&verified).expect("owner authority");
        assert_eq!(authority.root_key, compressed);
        assert_eq!(authority.epoch, binding.generation);
    }

    assert_eq!(
        verify_current_owner_binding(
            &binding,
            &owner_output(0, vector_bytes("owner_stale_program")),
        ),
        Err(ChatProtocolError::StaleOwner)
    );
    let stale_key: [u8; 33] = vector_bytes("owner_stale_compressed")
        .try_into()
        .expect("stale compressed owner key");
    let stale_address =
        Address::from_compressed_public_key(&stale_key).expect("stale owner address");
    assert_eq!(
        hex::encode(stale_address.hash),
        vector("owner_stale_program")
    );
    assert_eq!(
        verify_current_owner_binding(&binding, &owner_output(0, vec![0x55; 32])),
        Err(ChatProtocolError::UnsupportedOwnerScript)
    );
    assert_eq!(
        verify_current_owner_binding(&binding, &owner_output(1, vec![0x55; 20])),
        Err(ChatProtocolError::UnsupportedOwnerScript)
    );

    let invalid_generation = ChatIdentityBindingV1 {
        key_mode: ChatKeyMode::Owner,
        generation: 0,
        ..binding
    };
    assert!(invalid_generation.validate().is_err());
    assert!(
        verify_current_owner_binding(
            &invalid_generation,
            &owner_output(0, vector_bytes("owner_even_program")),
        )
        .is_err()
    );
    assert!(xonly_from_compressed_public_key(&[0; 33]).is_err());
}

#[test]
fn release_source_wire_vectors_are_exact_bounded_and_fail_closed() {
    assert_eq!(HNS_CHAT_WIRE_VERSION, 1);
    assert_eq!(MAX_CHAT_ENVELOPE_SIZE, 8_276);
    assert_eq!(MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE, 2_092);

    let envelope_bytes = vector_bytes("envelope_v1");
    let envelope = ChatEnvelopeV1::decode(&envelope_bytes).expect("canonical envelope");
    assert_eq!(envelope.message_id, [1; 32]);
    assert_eq!(envelope.created_at, 1_700_000_000);
    assert_eq!(envelope.expires_at, 1_700_000_600);
    assert_eq!(envelope.gift_wrap.as_slice(), b"opaque gift wrap");
    assert_eq!(envelope.encode().expect("encode envelope"), envelope_bytes);

    for name in [
        "invalid_envelope_version",
        "invalid_envelope_noncanonical_length",
        "invalid_envelope_trailing",
        "invalid_envelope_zero_message",
        "invalid_envelope_zero_recipient",
        "invalid_envelope_empty_gift_wrap",
        "invalid_envelope_oversize_declared",
    ] {
        assert!(
            ChatEnvelopeV1::decode(&vector_bytes(name)).is_err(),
            "accepted invalid release vector {name}"
        );
    }

    let acknowledgement_bytes = vector_bytes("acknowledgement_v1");
    let acknowledgement =
        ChatAcknowledgementV1::decode(&acknowledgement_bytes).expect("canonical acknowledgement");
    assert_eq!(acknowledgement.message_id, envelope.message_id);
    assert_eq!(acknowledgement.received_at, 1_700_000_100);
    assert_eq!(
        acknowledgement.encrypted_receipt.as_slice(),
        b"opaque receipt"
    );
    assert_eq!(
        acknowledgement.encode().expect("encode acknowledgement"),
        acknowledgement_bytes
    );

    for name in [
        "invalid_acknowledgement_version",
        "invalid_acknowledgement_noncanonical_length",
        "invalid_acknowledgement_trailing",
        "invalid_acknowledgement_zero_message",
        "invalid_acknowledgement_zero_time",
        "invalid_acknowledgement_empty_receipt",
        "invalid_acknowledgement_oversize_declared",
    ] {
        assert!(
            ChatAcknowledgementV1::decode(&vector_bytes(name)).is_err(),
            "accepted invalid release vector {name}"
        );
    }

    let mut invalid_time = envelope.clone();
    invalid_time.expires_at = invalid_time.created_at;
    assert!(invalid_time.validate().is_err());
    invalid_time.expires_at = invalid_time.created_at + MAX_CHAT_EXPIRATION_WINDOW + 1;
    assert!(invalid_time.validate().is_err());

    let maximum_envelope = ChatEnvelopeV1 {
        gift_wrap: vec![1; MAX_CHAT_CIPHERTEXT_SIZE],
        ..envelope
    };
    assert_eq!(
        maximum_envelope.encode().expect("maximum envelope").len(),
        MAX_CHAT_ENVELOPE_SIZE
    );
    let maximum_acknowledgement = ChatAcknowledgementV1 {
        encrypted_receipt: vec![1; MAX_CHAT_ACKNOWLEDGEMENT_SIZE],
        ..acknowledgement
    };
    assert_eq!(
        maximum_acknowledgement
            .encode()
            .expect("maximum acknowledgement")
            .len(),
        MAX_CHAT_ACKNOWLEDGEMENT_WIRE_SIZE
    );
}

#[test]
fn release_source_vector_sidecar_authenticates_packaged_bytes() {
    let expected = VECTORS_SHA256
        .split_whitespace()
        .next()
        .expect("fixture digest");
    assert_eq!(hex::encode(Sha256::digest(VECTORS.as_bytes())), expected);
}