#![allow(clippy::unwrap_used, clippy::missing_panics_doc, clippy::redundant_pub_crate)]
use alloc::vec::Vec;
use base64ct::{Base64, Encoding};
use ed25519_dalek::{Signer as _, SigningKey};
use crate::request::Nonce;
use crate::tags::{
TAG_CERT, TAG_DELE, TAG_INDX, TAG_MAXT, TAG_MIDP, TAG_MINT, TAG_PATH, TAG_PUBK, TAG_RADI,
TAG_ROOT, TAG_SIG, TAG_SREP,
};
const DELEGATION_SIGNATURE_CONTEXT: &[u8] = b"RoughTime v1 delegation signature--\x00";
const RESPONSE_SIGNATURE_CONTEXT: &[u8] = b"RoughTime v1 response signature\x00";
const ROOT_SEED: [u8; 32] = [1u8; 32];
const ONLINE_SEED: [u8; 32] = [2u8; 32];
fn encode_message(pairs: &[(u32, &[u8])]) -> Vec<u8> {
let n = pairs.len();
let mut out = Vec::new();
#[allow(clippy::cast_possible_truncation)]
out.extend_from_slice(&(n as u32).to_le_bytes());
let mut cumulative = 0u32;
for &(_, value) in &pairs[..n - 1] {
#[allow(clippy::cast_possible_truncation)]
{
cumulative += value.len() as u32;
}
out.extend_from_slice(&cumulative.to_le_bytes());
}
for &(tag, _) in pairs {
out.extend_from_slice(&tag.to_le_bytes());
}
for &(_, value) in pairs {
out.extend_from_slice(value);
}
out
}
fn truncated_sha512(parts: &[&[u8]]) -> [u8; 32] {
let mut buf = Vec::new();
for part in parts {
buf.extend_from_slice(part);
}
let full = crate::crypto::sha512(&buf);
let mut out = [0u8; 32];
out.copy_from_slice(&full[0..32]);
out
}
pub(crate) struct SignedResponse {
pub(crate) bytes: Vec<u8>,
pub(crate) root_pubkey_base64: alloc::string::String,
}
pub(crate) struct FixtureParams {
pub(crate) nonce: Nonce,
pub(crate) midpoint_micros: u64,
pub(crate) mint_micros: u64,
pub(crate) maxt_micros: u64,
pub(crate) index: u32,
pub(crate) path: Vec<[u8; 32]>,
pub(crate) corrupt_cert_sig: bool,
pub(crate) corrupt_response_sig: bool,
pub(crate) omit_tag: Option<u32>,
}
impl FixtureParams {
pub(crate) fn valid(nonce_byte: u8, midpoint_micros: u64) -> Self {
Self {
nonce: Nonce::new([nonce_byte; 64]),
midpoint_micros,
mint_micros: 0,
maxt_micros: u64::MAX,
index: 0,
path: Vec::new(),
corrupt_cert_sig: false,
corrupt_response_sig: false,
omit_tag: None,
}
}
}
pub(crate) fn build_signed_response(params: &FixtureParams) -> SignedResponse {
let root_key = SigningKey::from_bytes(&ROOT_SEED);
let online_key = SigningKey::from_bytes(&ONLINE_SEED);
let online_pubkey = online_key.verifying_key().to_bytes();
let mint_bytes = params.mint_micros.to_le_bytes();
let maxt_bytes = params.maxt_micros.to_le_bytes();
let dele_bytes = encode_message(&[
(TAG_PUBK, &online_pubkey),
(TAG_MINT, &mint_bytes),
(TAG_MAXT, &maxt_bytes),
]);
let mut dele_signed_data = Vec::with_capacity(DELEGATION_SIGNATURE_CONTEXT.len() + dele_bytes.len());
dele_signed_data.extend_from_slice(DELEGATION_SIGNATURE_CONTEXT);
dele_signed_data.extend_from_slice(&dele_bytes);
let mut cert_sig = root_key.sign(&dele_signed_data).to_bytes();
if params.corrupt_cert_sig {
cert_sig[0] ^= 0xFF;
}
let cert_bytes = encode_message(&[(TAG_DELE, &dele_bytes), (TAG_SIG, &cert_sig)]);
let mut current_hash = truncated_sha512(&[&[0x00], params.nonce.as_bytes().as_slice()]);
let mut idx = params.index;
let mut path_bytes = Vec::with_capacity(params.path.len() * 32);
for sibling in ¶ms.path {
current_hash = if idx & 1 == 0 {
truncated_sha512(&[&[0x01], ¤t_hash, sibling])
} else {
truncated_sha512(&[&[0x01], sibling, ¤t_hash])
};
path_bytes.extend_from_slice(sibling);
idx >>= 1;
}
let root_hash = current_hash;
let midpoint_bytes = params.midpoint_micros.to_le_bytes();
let radius_bytes = 1_000_000u32.to_le_bytes();
let srep_bytes = encode_message(&[
(TAG_ROOT, &root_hash),
(TAG_MIDP, &midpoint_bytes),
(TAG_RADI, &radius_bytes),
]);
let mut srep_signed_data = Vec::with_capacity(RESPONSE_SIGNATURE_CONTEXT.len() + srep_bytes.len());
srep_signed_data.extend_from_slice(RESPONSE_SIGNATURE_CONTEXT);
srep_signed_data.extend_from_slice(&srep_bytes);
let mut sig = online_key.sign(&srep_signed_data).to_bytes();
if params.corrupt_response_sig {
sig[0] ^= 0xFF;
}
let index_bytes = params.index.to_le_bytes();
let mut pairs: Vec<(u32, &[u8])> = alloc::vec![
(TAG_SIG, &sig),
(TAG_PATH, &path_bytes),
(TAG_CERT, &cert_bytes),
(TAG_INDX, &index_bytes),
(TAG_SREP, &srep_bytes),
];
if let Some(omit) = params.omit_tag {
pairs.retain(|&(tag, _)| tag != omit);
}
let bytes = encode_message(&pairs);
let root_pubkey_base64 = Base64::encode_string(&root_key.verifying_key().to_bytes());
SignedResponse {
bytes,
root_pubkey_base64,
}
}
pub(crate) fn root_pubkey_base64() -> alloc::string::String {
let root_key = SigningKey::from_bytes(&ROOT_SEED);
Base64::encode_string(&root_key.verifying_key().to_bytes())
}
pub(crate) fn root_pubkey_base64_static() -> &'static str {
alloc::boxed::Box::leak(root_pubkey_base64().into_boxed_str())
}