#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
use crate::keys::identity::Ed25519Identity;
use tempfile::tempdir;
const LOW_ORDER_POINT_ORDER_8: [u8; 32] = [
0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4, 0x6a,
0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00,
];
fn identity() -> Ed25519Identity {
let dir = tempdir().unwrap();
Ed25519Identity::load_or_generate(&dir.path().join("id.key")).unwrap()
}
#[test]
fn seal_returns_envelope_with_expected_field_shapes() {
let recipient = identity();
let payload = b"sealed credential bundle";
let env = seal(payload, &recipient.x25519_public_key()).unwrap();
assert_eq!(env.ephemeral_public_key.len(), 32);
assert_eq!(env.nonce.len(), 12);
assert_eq!(env.ciphertext.len(), payload.len() + 16);
}
#[test]
fn seal_open_roundtrip_returns_original_payload() {
let recipient = identity();
let payload = b"API token: super-secret";
let env = seal(payload, &recipient.x25519_public_key()).unwrap();
let opened = open(&env, &recipient.x25519_static_secret()).unwrap();
assert_eq!(opened.as_slice(), payload);
}
#[test]
fn open_with_wrong_private_key_returns_unseal_failed() {
let recipient = identity();
let wrong = identity();
let env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
let err = open(&env, &wrong.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn tampered_ciphertext_returns_unseal_failed() {
let recipient = identity();
let mut env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
env.ciphertext[0] ^= 0x01;
let err = open(&env, &recipient.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn tampered_nonce_returns_unseal_failed() {
let recipient = identity();
let mut env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
env.nonce[0] ^= 0x01;
let err = open(&env, &recipient.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn tampered_ephemeral_key_returns_unseal_failed() {
let recipient = identity();
let mut env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
let other = identity();
env.ephemeral_public_key = other.x25519_public_key();
let err = open(&env, &recipient.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn low_order_ephemeral_key_all_zero_returns_unseal_failed() {
let recipient = identity();
let mut env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
env.ephemeral_public_key = [0u8; 32];
let err = open(&env, &recipient.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn low_order_ephemeral_key_order_8_returns_unseal_failed() {
let recipient = identity();
let mut env = seal(b"payload", &recipient.x25519_public_key()).unwrap();
env.ephemeral_public_key = LOW_ORDER_POINT_ORDER_8;
let err = open(&env, &recipient.x25519_static_secret()).unwrap_err();
assert!(matches!(err, TrustError::UnsealFailed));
}
#[test]
fn sealing_to_low_order_recipient_key_returns_seal_error() {
for low_order in [[0u8; 32], LOW_ORDER_POINT_ORDER_8] {
let err = seal(b"payload", &low_order).unwrap_err();
match err {
TrustError::Seal { reason } => {
assert!(
reason.contains("low-order"),
"unexpected seal error reason: {reason}"
);
}
other => panic!("expected TrustError::Seal, got {other:?}"),
}
}
}
#[test]
fn two_seals_of_same_payload_produce_distinct_ciphertexts_and_keys() {
let recipient = identity();
let payload = b"identical payload";
let env_a = seal(payload, &recipient.x25519_public_key()).unwrap();
let env_b = seal(payload, &recipient.x25519_public_key()).unwrap();
assert_ne!(env_a.ephemeral_public_key, env_b.ephemeral_public_key);
assert_ne!(env_a.ciphertext, env_b.ciphertext);
assert_ne!(env_a.nonce, env_b.nonce);
}
#[test]
fn empty_payload_seals_and_opens() {
let recipient = identity();
let env = seal(&[], &recipient.x25519_public_key()).unwrap();
assert_eq!(env.ciphertext.len(), 16);
let opened = open(&env, &recipient.x25519_static_secret()).unwrap();
assert!(opened.is_empty());
}
#[test]
fn sealed_envelope_round_trips_through_postcard() {
let recipient = identity();
let env = seal(b"persisted", &recipient.x25519_public_key()).unwrap();
let bytes = postcard::to_allocvec(&env).unwrap();
let restored: SealedEnvelope = postcard::from_bytes(&bytes).unwrap();
assert_eq!(restored, env);
let opened = open(&restored, &recipient.x25519_static_secret()).unwrap();
assert_eq!(opened.as_slice(), b"persisted");
}
#[test]
fn sealed_envelope_json_shape_is_frozen() {
let envelope = SealedEnvelope {
ephemeral_public_key: [1u8; 32],
ciphertext: vec![0xde, 0xad, 0xbe, 0xef],
nonce: [2u8; NONCE_LEN],
};
let value = serde_json::to_value(&envelope).unwrap();
let object = value
.as_object()
.expect("a sealed envelope serialises as a JSON object");
let mut keys: Vec<&str> = object.keys().map(String::as_str).collect();
keys.sort_unstable();
assert_eq!(
keys,
["ciphertext", "ephemeral_public_key", "nonce"],
"the frozen JSON field set changed"
);
assert_eq!(
object["ephemeral_public_key"],
serde_json::json!(vec![1u8; 32])
);
assert_eq!(
object["ciphertext"],
serde_json::json!([0xde, 0xad, 0xbe, 0xef])
);
assert_eq!(object["nonce"], serde_json::json!(vec![2u8; NONCE_LEN]));
let from_wire: SealedEnvelope = serde_json::from_str(
r#"{
"ephemeral_public_key":
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
"ciphertext": [222,173,190,239],
"nonce": [2,2,2,2,2,2,2,2,2,2,2,2]
}"#,
)
.unwrap();
assert_eq!(from_wire, envelope);
}
#[test]
fn attestation_bytes_concatenates_in_declared_order() {
let recipient = identity();
let env = seal(b"x", &recipient.x25519_public_key()).unwrap();
let bytes = env.attestation_bytes();
let expected_len = env.ephemeral_public_key.len() + env.ciphertext.len() + env.nonce.len();
assert_eq!(bytes.len(), expected_len);
assert_eq!(&bytes[..32], env.ephemeral_public_key.as_slice());
assert_eq!(
&bytes[32..32 + env.ciphertext.len()],
env.ciphertext.as_slice()
);
assert_eq!(&bytes[32 + env.ciphertext.len()..], env.nonce.as_slice());
}