prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use alloc::{format, vec};

use super::*;
use crate::identity::in_memory::InMemoryNodeIdentity;
use crate::identity::{IdentitySigner, IDENTITY_SECRET_KEY_LEN};

#[test]
fn credentials_derive_authentication_and_transport_authority_together() {
    let secret = [0x42; IDENTITY_SECRET_KEY_LEN];
    let identity = InMemoryNodeIdentity::from_secret_key_bytes(&secret);
    let rpc_key = RpcAuthenticationKey::from_rns_transport_identity_secret(&secret);
    let credentials = SharedInstanceCredentials::from_identity_secret(&secret);

    assert_eq!(rpc_key.as_bytes(), crate::crypto::sha256(&secret));
    assert_eq!(credentials.rpc_key().as_bytes(), rpc_key.as_bytes());
    assert_eq!(
        credentials.transport_identity_hash(),
        identity.identity_hash()
    );
    assert!(!format!("{:?}", credentials.rpc_key()).contains("4242"));
}

#[test]
fn frame_headers_use_cpython_short_and_wide_forms() {
    let short = EncodedRpcFrameHeader::new(1_024).unwrap();
    assert_eq!(short.as_bytes(), &1_024i32.to_be_bytes());
    let RpcFrameHeaderPrefix::Complete(short_length) =
        RpcFrameHeaderPrefix::decode(short.as_bytes().try_into().unwrap()).unwrap()
    else {
        panic!("short header carries its complete length");
    };
    assert_eq!(short_length.as_usize(), 1_024);

    let wide_length = i32::MAX as usize + 1;
    let wide = EncodedRpcFrameHeader::new(wide_length).unwrap();
    assert_eq!(&wide.as_bytes()[..4], &(-1i32).to_be_bytes());
    assert_eq!(
        RpcFrameHeaderPrefix::decode(wide.as_bytes()[..4].try_into().unwrap()),
        Ok(RpcFrameHeaderPrefix::WideLengthFollows)
    );
    assert_eq!(
        RpcFrameHeaderPrefix::decode_wide(wide.as_bytes()[4..].try_into().unwrap())
            .unwrap()
            .as_usize(),
        wide_length
    );
}

#[test]
fn negative_short_frame_lengths_are_rejected() {
    assert_eq!(
        RpcFrameHeaderPrefix::decode((-2i32).to_be_bytes()),
        Err(RpcFrameLengthDecodeError::NegativeShortLength)
    );
}

#[test]
fn modern_server_challenge_accepts_sha256_and_legacy_md5_responses() {
    let key = RpcAuthenticationKey::new(vec![0x5a; 32]);
    let challenge = RpcServerChallenge::new(RpcChallengeNonce::new([0x11; 40]));
    let message = challenge
        .wire_payload()
        .strip_prefix(b"#CHALLENGE#")
        .unwrap();

    let mut modern = b"{sha256}".to_vec();
    modern.extend_from_slice(
        &RpcDigest::Sha256
            .message_authentication_code(&key, message)
            .unwrap(),
    );
    assert_eq!(
        challenge.authenticate_response(&key, &modern),
        Ok(RpcAuthenticationVerdict::Authenticated)
    );

    let legacy = RpcDigest::Md5
        .message_authentication_code(&key, message)
        .unwrap();
    assert_eq!(
        challenge.authenticate_response(&key, &legacy),
        Ok(RpcAuthenticationVerdict::Authenticated)
    );
}

#[test]
fn authentication_matches_cpython_reference_vectors() {
    let key = RpcAuthenticationKey::new(vec![0x5a; 32]);
    let challenge = RpcServerChallenge::new(RpcChallengeNonce::new([0x11; 40]));
    let mut sha256_response = b"{sha256}".to_vec();
    sha256_response.extend_from_slice(&[
        0x49, 0xba, 0xf8, 0x4c, 0x6a, 0x22, 0x32, 0x53, 0x90, 0x38, 0x1d, 0xf0, 0x9d, 0x94, 0x93,
        0xfe, 0x9a, 0x77, 0xcf, 0x05, 0x0d, 0xde, 0x03, 0x92, 0x22, 0x14, 0x0e, 0x17, 0xe9, 0xd8,
        0x80, 0x90,
    ]);
    assert_eq!(
        challenge.authenticate_response(&key, &sha256_response),
        Ok(RpcAuthenticationVerdict::Authenticated)
    );
    assert_eq!(
        challenge.authenticate_response(
            &key,
            &[
                0x1f, 0xb3, 0xcb, 0x62, 0xba, 0xc0, 0xe1, 0x81, 0x60, 0x61, 0xcb, 0xcb, 0xc6, 0x2b,
                0x20, 0xae,
            ],
        ),
        Ok(RpcAuthenticationVerdict::Authenticated)
    );

    let tagged = RpcClientChallenge::parse(b"#CHALLENGE#{md5}client nonce")
        .unwrap()
        .response(&key)
        .unwrap();
    assert_eq!(
        tagged.wire_payload(),
        &[
            b'{', b'm', b'd', b'5', b'}', 0x32, 0xdd, 0x49, 0x9b, 0x5a, 0xd5, 0x54, 0x93, 0xa2,
            0x76, 0xbe, 0x95, 0x9b, 0xe0, 0x07, 0x11,
        ]
    );
    let legacy = RpcClientChallenge::parse(&[
        b'#', b'C', b'H', b'A', b'L', b'L', b'E', b'N', b'G', b'E', b'#', 0x22, 0x22, 0x22, 0x22,
        0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
        0x22,
    ])
    .unwrap()
    .response(&key)
    .unwrap();
    assert_eq!(
        legacy.wire_payload(),
        &[
            0x4b, 0x29, 0x15, 0x08, 0xfe, 0x87, 0x8d, 0xf0, 0x25, 0xc0, 0x31, 0xe6, 0xa5, 0x32,
            0xfc, 0x17,
        ]
    );
}

#[test]
fn client_challenge_answers_the_negotiated_digest() {
    let key = RpcAuthenticationKey::new(vec![0x5a; 32]);
    let mut tagged = b"#CHALLENGE#{md5}".to_vec();
    tagged.extend_from_slice(b"client nonce");
    let response = RpcClientChallenge::parse(&tagged)
        .unwrap()
        .response(&key)
        .unwrap();
    assert!(response.wire_payload().starts_with(b"{md5}"));

    let mut legacy = b"#CHALLENGE#".to_vec();
    legacy.extend_from_slice(&[0x22; LEGACY_MD5_MESSAGE_LENGTH]);
    let response = RpcClientChallenge::parse(&legacy)
        .unwrap()
        .response(&key)
        .unwrap();
    assert_eq!(response.wire_payload().len(), LEGACY_MD5_DIGEST_LENGTH);
}

#[test]
fn unsupported_digests_and_bad_macs_are_rejected() {
    let key = RpcAuthenticationKey::new(vec![0x5a; 32]);
    let challenge = RpcServerChallenge::new(RpcChallengeNonce::new([0x11; 40]));
    let mut bad_mac = b"{sha256}".to_vec();
    bad_mac.extend_from_slice(&[0; 32]);

    assert_eq!(
        challenge.authenticate_response(&key, &bad_mac),
        Ok(RpcAuthenticationVerdict::Rejected)
    );
    assert_eq!(
        RpcClientChallenge::parse(b"#CHALLENGE#{sha1}payload"),
        Err(RpcAuthenticationError::UnsupportedDigest)
    );
}