use serde::{Deserialize, Serialize};
use crate::{ControlCommand, RekeyParams};
pub fn encode_prepare_upgrade(
payload: &[u8],
payload_signature: &[u8; 64],
rekey: Option<RekeyParams>,
nonce: [u8; 32],
) -> Vec<u8> {
let cmd = ControlCommand::PrepareUpgrade {
payload: payload.to_vec(),
payload_signature: payload_signature.to_vec(),
rekey,
nonce,
};
encode_command(&cmd)
}
pub fn encode_revoke_upgrade(
payload: &[u8],
payload_signature: &[u8; 64],
rollback: bool,
nonce: [u8; 32],
) -> Vec<u8> {
let cmd = ControlCommand::RevokeUpgrade {
payload: payload.to_vec(),
payload_signature: payload_signature.to_vec(),
rollback,
nonce,
};
encode_command(&cmd)
}
fn encode_command(cmd: &ControlCommand) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(cmd, &mut buf)
.expect("CBOR encoding a ControlCommand into a Vec cannot fail");
buf
}
#[derive(Debug, thiserror::Error)]
pub enum DerSignatureError {
#[error("invalid DER ECDSA P-256 signature: {0}")]
InvalidDer(#[source] p256::ecdsa::Error),
}
pub fn der_signature_to_raw(der: &[u8]) -> Result<[u8; 64], DerSignatureError> {
let sig = p256::ecdsa::Signature::from_der(der).map_err(DerSignatureError::InvalidDer)?;
let sig = sig.normalize_s().unwrap_or(sig);
let mut out = [0u8; 64];
out.copy_from_slice(&sig.to_bytes());
Ok(out)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmPrepareResponse {
#[serde(with = "base64_vec")]
pub payload: Vec<u8>,
#[serde(with = "base64_array32")]
pub nonce: [u8; 32],
pub rekey: Option<RekeyParams>,
pub valid_from: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfirmSubmitRequest {
#[serde(with = "base64_vec")]
pub command: Vec<u8>,
#[serde(with = "base64_vec")]
pub envelope_signature: Vec<u8>,
}
pub type RevokeSubmitRequest = ConfirmSubmitRequest;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevokePrepareResponse {
#[serde(with = "base64_vec")]
pub payload: Vec<u8>,
#[serde(with = "base64_array32")]
pub nonce: [u8; 32],
pub rollback: bool,
}
mod base64_vec {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &Vec<u8>, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&STANDARD.encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<u8>, D::Error> {
let s = String::deserialize(de)?;
STANDARD.decode(s.as_bytes()).map_err(serde::de::Error::custom)
}
}
mod base64_array32 {
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8; 32], ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&STANDARD.encode(bytes))
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 32], D::Error> {
let s = String::deserialize(de)?;
let v = STANDARD.decode(s.as_bytes()).map_err(serde::de::Error::custom)?;
v.try_into()
.map_err(|v: Vec<u8>| serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len())))
}
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::signature::{Signer, Verifier};
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
fn sample_rekey() -> RekeyParams {
RekeyParams {
new_public_key: vec![0xAB; 70],
new_key_id: "arn:aws:kms:us-east-1:123:key/abc".into(),
}
}
fn backend_style_encode(cmd: &ControlCommand) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(cmd, &mut buf).unwrap();
buf
}
#[test]
fn encode_prepare_upgrade_round_trips_and_matches_backend_encoding() {
let payload = vec![1u8, 2, 3, 4];
let payload_sig = [0xDEu8; 64];
let nonce = [0x42u8; 32];
let bytes = encode_prepare_upgrade(&payload, &payload_sig, Some(sample_rekey()), nonce);
let expected = backend_style_encode(&ControlCommand::PrepareUpgrade {
payload: payload.clone(),
payload_signature: payload_sig.to_vec(),
rekey: Some(sample_rekey()),
nonce,
});
assert_eq!(bytes, expected);
let back: ControlCommand = ciborium::from_reader(bytes.as_slice()).unwrap();
match back {
ControlCommand::PrepareUpgrade {
payload: p,
payload_signature: ps,
rekey,
nonce: n,
} => {
assert_eq!(p, payload);
assert_eq!(ps, payload_sig.to_vec());
let rk = rekey.expect("rekey present");
assert_eq!(rk.new_public_key, vec![0xAB; 70]);
assert_eq!(rk.new_key_id, "arn:aws:kms:us-east-1:123:key/abc");
assert_eq!(n, nonce);
}
_ => panic!("wrong variant"),
}
}
#[test]
fn encode_prepare_upgrade_stateless_matches_backend_encoding() {
let bytes = encode_prepare_upgrade(&[0xAA], &[0xBB; 64], None, [1u8; 32]);
let expected = backend_style_encode(&ControlCommand::PrepareUpgrade {
payload: vec![0xAA],
payload_signature: vec![0xBB; 64],
rekey: None,
nonce: [1u8; 32],
});
assert_eq!(bytes, expected);
}
#[test]
fn encode_revoke_upgrade_round_trips_and_matches_backend_encoding() {
let payload = vec![0xCCu8; 8];
let payload_sig = [0xDDu8; 64];
let nonce = [0x99u8; 32];
for rollback in [true, false] {
let bytes = encode_revoke_upgrade(&payload, &payload_sig, rollback, nonce);
let expected = backend_style_encode(&ControlCommand::RevokeUpgrade {
payload: payload.clone(),
payload_signature: payload_sig.to_vec(),
rollback,
nonce,
});
assert_eq!(bytes, expected);
let back: ControlCommand = ciborium::from_reader(bytes.as_slice()).unwrap();
match back {
ControlCommand::RevokeUpgrade {
payload: p,
rollback: rb,
nonce: n,
..
} => {
assert_eq!(p, payload);
assert_eq!(rb, rollback);
assert_eq!(n, nonce);
}
_ => panic!("wrong variant"),
}
}
}
#[test]
fn der_signature_to_raw_matches_direct_raw_encoding() {
let sk = SigningKey::from_bytes(&[7u8; 32].into()).unwrap();
let msg = b"custody der test vector";
let sig: Signature = sk.sign(msg);
let low = sig.normalize_s().unwrap_or(sig);
let raw = der_signature_to_raw(sig.to_der().as_bytes()).unwrap();
assert_eq!(raw, <[u8; 64]>::try_from(&low.to_bytes()[..]).unwrap());
let vk = VerifyingKey::from(&sk);
let parsed = Signature::from_slice(&raw).unwrap();
vk.verify(msg, &parsed).unwrap();
}
#[test]
fn der_signature_to_raw_normalizes_high_s() {
let sk = SigningKey::from_bytes(&[9u8; 32].into()).unwrap();
let msg = b"high-s normalization vector";
let sig: Signature = sk.sign(msg);
let low = sig.normalize_s().unwrap_or(sig);
let (r, s) = low.split_scalars();
let high_s = -*s.as_ref();
let high = Signature::from_scalars(r.to_bytes(), high_s.to_bytes()).unwrap();
assert!(high.normalize_s().is_some(), "twin must be high-S");
let raw = der_signature_to_raw(high.to_der().as_bytes()).unwrap();
assert_eq!(raw, <[u8; 64]>::try_from(&low.to_bytes()[..]).unwrap());
let vk = VerifyingKey::from(&sk);
let parsed = Signature::from_slice(&raw).unwrap();
vk.verify(msg, &parsed).unwrap();
}
#[test]
fn der_signature_to_raw_rejects_garbage() {
assert!(der_signature_to_raw(&[0u8; 64]).is_err());
assert!(der_signature_to_raw(b"not der").is_err());
assert!(der_signature_to_raw(&[]).is_err());
}
#[test]
fn confirm_prepare_response_serde_round_trip() {
let resp = ConfirmPrepareResponse {
payload: vec![1, 2, 3],
nonce: [0x11u8; 32],
rekey: Some(sample_rekey()),
valid_from: "2026-07-09T00:00:00Z".into(),
};
let json = serde_json::to_string(&resp).unwrap();
let back: ConfirmPrepareResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload, resp.payload);
assert_eq!(back.nonce, resp.nonce);
assert_eq!(back.valid_from, resp.valid_from);
let rk = back.rekey.as_ref().unwrap();
assert_eq!(rk.new_public_key, vec![0xAB; 70]);
assert_eq!(rk.new_key_id, "arn:aws:kms:us-east-1:123:key/abc");
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(v["payload"].is_string());
assert!(v["nonce"].is_string());
}
#[test]
fn rekey_survives_json_round_trip_byte_exactly() {
let payload = vec![5u8; 16];
let payload_sig = [0x77u8; 64];
let nonce = [0x33u8; 32];
let backend_cmd =
encode_prepare_upgrade(&payload, &payload_sig, Some(sample_rekey()), nonce);
let resp = ConfirmPrepareResponse {
payload,
nonce,
rekey: Some(sample_rekey()),
valid_from: "2026-07-09T00:00:00Z".into(),
};
let json = serde_json::to_string(&resp).unwrap();
let back: ConfirmPrepareResponse = serde_json::from_str(&json).unwrap();
let cli_cmd =
encode_prepare_upgrade(&back.payload, &payload_sig, back.rekey, back.nonce);
assert_eq!(cli_cmd, backend_cmd);
}
#[test]
fn confirm_submit_request_serde_round_trip() {
let req = ConfirmSubmitRequest {
command: vec![9, 8, 7],
envelope_signature: vec![0x55; 64],
};
let json = serde_json::to_string(&req).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(v["command"].is_string());
assert!(v["envelope_signature"].is_string());
let back: ConfirmSubmitRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.command, req.command);
assert_eq!(back.envelope_signature, req.envelope_signature);
}
#[test]
fn revoke_prepare_response_serde_round_trip() {
let resp = RevokePrepareResponse {
payload: vec![4, 5, 6],
nonce: [0x22u8; 32],
rollback: true,
};
let json = serde_json::to_string(&resp).unwrap();
let back: RevokePrepareResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.payload, resp.payload);
assert_eq!(back.nonce, resp.nonce);
assert!(back.rollback);
}
#[test]
fn nonce_deserialize_rejects_wrong_length() {
let json = format!(
r#"{{"payload":"AQID","nonce":"{}","rekey":null,"valid_from":"x"}}"#,
{
use base64::Engine as _;
base64::engine::general_purpose::STANDARD.encode([0u8; 31])
}
);
assert!(serde_json::from_str::<ConfirmPrepareResponse>(&json).is_err());
}
}