use crate::{Result, SealedError};
use alloc::format;
use alloc::vec::Vec;
use crate::derive::ED25519_PUBLIC_KEY_LEN;
pub const ED25519_SIGNATURE_LEN: usize = 64;
pub const IDENTITY_ASSERTION_MIN_LEN: usize = ED25519_PUBLIC_KEY_LEN + ED25519_SIGNATURE_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IdentityAssertion<'a> {
pub public_key: &'a [u8],
pub signature: &'a [u8],
pub signed_data: &'a [u8],
}
pub fn encode_identity_assertion(
public_key: &[u8],
signature: &[u8],
signed_data: &[u8],
) -> Result<Vec<u8>> {
if public_key.len() != ED25519_PUBLIC_KEY_LEN {
return Err(SealedError::InvalidPublicKey(format!(
"Ed25519 public key must be {} bytes, got {}",
ED25519_PUBLIC_KEY_LEN,
public_key.len()
)));
}
if signature.len() != ED25519_SIGNATURE_LEN {
return Err(SealedError::Serialization(format!(
"Identity assertion signature must be {} bytes, got {}",
ED25519_SIGNATURE_LEN,
signature.len()
)));
}
let mut out = Vec::with_capacity(IDENTITY_ASSERTION_MIN_LEN + signed_data.len());
out.extend_from_slice(public_key);
out.extend_from_slice(signature);
out.extend_from_slice(signed_data);
Ok(out)
}
pub fn parse_identity_assertion(bytes: &[u8]) -> Result<IdentityAssertion<'_>> {
if bytes.len() < IDENTITY_ASSERTION_MIN_LEN {
return Err(SealedError::Deserialization(format!(
"Identity assertion is {} bytes, minimum is {}",
bytes.len(),
IDENTITY_ASSERTION_MIN_LEN
)));
}
let (public_key, rest) = bytes.split_at(ED25519_PUBLIC_KEY_LEN);
let (signature, signed_data) = rest.split_at(ED25519_SIGNATURE_LEN);
Ok(IdentityAssertion {
public_key,
signature,
signed_data,
})
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn encode_then_parse_returns_the_fields() {
let key = [1u8; 32];
let sig = [2u8; 64];
let data = b"whatever follows";
let bytes = encode_identity_assertion(&key, &sig, data).expect("encodes");
assert_eq!(bytes.len(), 96 + data.len());
let parsed = parse_identity_assertion(&bytes).expect("parses");
assert_eq!(parsed.public_key, &key);
assert_eq!(parsed.signature, &sig);
assert_eq!(parsed.signed_data, data);
}
#[test]
fn an_empty_signed_data_is_the_floor_and_is_accepted() {
let bytes = encode_identity_assertion(&[0u8; 32], &[0u8; 64], &[]).expect("encodes");
assert_eq!(bytes.len(), IDENTITY_ASSERTION_MIN_LEN);
let parsed = parse_identity_assertion(&bytes).expect("96 bytes is a whole assertion");
assert!(parsed.signed_data.is_empty());
}
#[test]
fn encode_refuses_wrong_field_sizes() {
for key_len in [0usize, 31, 33] {
let err = encode_identity_assertion(&vec![0u8; key_len], &[0u8; 64], &[]).unwrap_err();
assert!(
matches!(err, SealedError::InvalidPublicKey(_)),
"{key_len}: {err}"
);
}
for sig_len in [0usize, 63, 65] {
let err = encode_identity_assertion(&[0u8; 32], &vec![0u8; sig_len], &[]).unwrap_err();
assert!(
matches!(err, SealedError::Serialization(_)),
"{sig_len}: {err}"
);
}
}
#[test]
fn parse_refuses_everything_under_the_floor() {
for len in [0usize, 1, 32, 64, 95] {
let err = parse_identity_assertion(&vec![0u8; len]).unwrap_err();
assert!(
matches!(err, SealedError::Deserialization(_)),
"{len}: {err}"
);
}
}
}
#[cfg(all(test, feature = "std"))]
mod spec_vectors {
use super::*;
const VECTORS: &str = include_str!("../tests/data/identity-assertion-v1.vectors.json");
fn vectors() -> serde_json::Value {
serde_json::from_str(VECTORS).expect("the vector file is JSON")
}
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("hex digit"))
.collect()
}
#[test]
fn the_vector_file_is_the_size_it_was() {
let v = vectors();
assert_eq!(v["assertions"].as_array().expect("assertions").len(), 3);
assert_eq!(v["refusals"].as_array().expect("refusals").len(), 3);
assert_eq!(v["minimum_length"], IDENTITY_ASSERTION_MIN_LEN);
assert!(
v["assertions"]
.as_array()
.expect("assertions")
.iter()
.any(|a| a["signed_data_hex"] == ""),
"one assertion must sit exactly on the floor, or the file cannot catch \
a reader that refuses 96 bytes"
);
}
#[test]
fn every_assertion_parses_into_the_fields_it_was_built_from() {
for case in vectors()["assertions"].as_array().expect("assertions") {
let name = case["name"].as_str().expect("a name");
let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
let parsed =
parse_identity_assertion(&bytes).unwrap_or_else(|e| panic!("[{name}]: {e}"));
assert_eq!(
parsed.public_key,
unhex(case["public_key_hex"].as_str().expect("key")),
"[{name}] public key"
);
assert_eq!(
parsed.signature,
unhex(case["signature_hex"].as_str().expect("signature")),
"[{name}] signature"
);
assert_eq!(
parsed.signed_data,
unhex(case["signed_data_hex"].as_str().expect("signed data")),
"[{name}] signed data"
);
assert_eq!(
crate::derive_address(parsed.public_key)
.expect("32-byte key")
.to_string(),
case["address"].as_str().expect("address"),
"[{name}] the address the chapter's derivation gives this key"
);
}
}
#[test]
fn every_assertion_re_encodes_to_the_same_bytes() {
for case in vectors()["assertions"].as_array().expect("assertions") {
let name = case["name"].as_str().expect("a name");
let want = unhex(case["assertion_hex"].as_str().expect("hex"));
let got = encode_identity_assertion(
&unhex(case["public_key_hex"].as_str().expect("key")),
&unhex(case["signature_hex"].as_str().expect("signature")),
&unhex(case["signed_data_hex"].as_str().expect("signed data")),
)
.unwrap_or_else(|e| panic!("[{name}]: {e}"));
assert_eq!(got, want, "[{name}] encoded bytes");
}
}
#[test]
fn every_refusal_is_refused() {
for case in vectors()["refusals"].as_array().expect("refusals") {
let name = case["name"].as_str().expect("a name");
let bytes = unhex(case["assertion_hex"].as_str().expect("hex"));
assert!(
parse_identity_assertion(&bytes).is_err(),
"[{name}] {} bytes must be refused",
bytes.len()
);
}
}
fn chapter() -> Option<String> {
let path =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/spec/ble-framing.md");
std::fs::read_to_string(&path).ok().or_else(|| {
eprintln!("spec tree not present, skipping the BLE framing chapter drift checks");
None
})
}
#[test]
fn the_chapter_states_the_layout_the_code_performs() {
let Some(text) = chapter() else { return };
assert!(
text.contains("| `public_key` | 32 |") && text.contains("| `signature` | 64 |"),
"the chapter no longer states the two fixed field sizes"
);
assert!(
text.contains(&format!("shorter than {IDENTITY_ASSERTION_MIN_LEN} bytes")),
"the chapter no longer states the {IDENTITY_ASSERTION_MIN_LEN}-byte floor"
);
assert!(
text.contains("identity-assertion-v1.vectors.json"),
"the chapter no longer names the vector file that pins it"
);
}
}