1use serde::{Deserialize, Serialize};
19
20use crate::{ControlCommand, RekeyParams};
21
22pub fn encode_prepare_upgrade(
35 payload: &[u8],
36 payload_signature: &[u8; 64],
37 rekey: Option<RekeyParams>,
38 nonce: [u8; 32],
39) -> Vec<u8> {
40 let cmd = ControlCommand::PrepareUpgrade {
41 payload: payload.to_vec(),
42 payload_signature: payload_signature.to_vec(),
43 rekey,
44 nonce,
45 };
46 encode_command(&cmd)
47}
48
49pub fn encode_revoke_upgrade(
53 payload: &[u8],
54 payload_signature: &[u8; 64],
55 rollback: bool,
56 nonce: [u8; 32],
57) -> Vec<u8> {
58 let cmd = ControlCommand::RevokeUpgrade {
59 payload: payload.to_vec(),
60 payload_signature: payload_signature.to_vec(),
61 rollback,
62 nonce,
63 };
64 encode_command(&cmd)
65}
66
67fn encode_command(cmd: &ControlCommand) -> Vec<u8> {
72 let mut buf = Vec::new();
73 ciborium::into_writer(cmd, &mut buf)
74 .expect("CBOR encoding a ControlCommand into a Vec cannot fail");
75 buf
76}
77
78#[derive(Debug, thiserror::Error)]
80pub enum DerSignatureError {
81 #[error("invalid DER ECDSA P-256 signature: {0}")]
83 InvalidDer(#[source] p256::ecdsa::Error),
84}
85
86pub fn der_signature_to_raw(der: &[u8]) -> Result<[u8; 64], DerSignatureError> {
94 let sig = p256::ecdsa::Signature::from_der(der).map_err(DerSignatureError::InvalidDer)?;
95 let sig = sig.normalize_s().unwrap_or(sig);
96 let mut out = [0u8; 64];
97 out.copy_from_slice(&sig.to_bytes());
98 Ok(out)
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct ConfirmPrepareResponse {
117 #[serde(with = "base64_vec")]
119 pub payload: Vec<u8>,
120 #[serde(with = "base64_array32")]
124 pub nonce: [u8; 32],
125 pub rekey: Option<RekeyParams>,
127 pub valid_from: String,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ConfirmSubmitRequest {
139 #[serde(with = "base64_vec")]
142 pub command: Vec<u8>,
143 #[serde(with = "base64_vec")]
145 pub envelope_signature: Vec<u8>,
146}
147
148pub type RevokeSubmitRequest = ConfirmSubmitRequest;
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RevokePrepareResponse {
157 #[serde(with = "base64_vec")]
159 pub payload: Vec<u8>,
160 #[serde(with = "base64_array32")]
162 pub nonce: [u8; 32],
163 pub rollback: bool,
167}
168
169mod base64_vec {
172 use base64::engine::general_purpose::STANDARD;
173 use base64::Engine as _;
174 use serde::{Deserialize, Deserializer, Serializer};
175
176 pub fn serialize<S: Serializer>(bytes: &Vec<u8>, ser: S) -> Result<S::Ok, S::Error> {
177 ser.serialize_str(&STANDARD.encode(bytes))
178 }
179
180 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<u8>, D::Error> {
181 let s = String::deserialize(de)?;
182 STANDARD.decode(s.as_bytes()).map_err(serde::de::Error::custom)
183 }
184}
185
186mod base64_array32 {
189 use base64::engine::general_purpose::STANDARD;
190 use base64::Engine as _;
191 use serde::{Deserialize, Deserializer, Serializer};
192
193 pub fn serialize<S: Serializer>(bytes: &[u8; 32], ser: S) -> Result<S::Ok, S::Error> {
194 ser.serialize_str(&STANDARD.encode(bytes))
195 }
196
197 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 32], D::Error> {
198 let s = String::deserialize(de)?;
199 let v = STANDARD.decode(s.as_bytes()).map_err(serde::de::Error::custom)?;
200 v.try_into()
201 .map_err(|v: Vec<u8>| serde::de::Error::custom(format!("expected 32 bytes, got {}", v.len())))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use p256::ecdsa::signature::{Signer, Verifier};
209 use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
210
211 fn sample_rekey() -> RekeyParams {
212 RekeyParams {
213 new_public_key: vec![0xAB; 70],
214 new_key_id: "arn:aws:kms:us-east-1:123:key/abc".into(),
215 }
216 }
217
218 fn backend_style_encode(cmd: &ControlCommand) -> Vec<u8> {
222 let mut buf = Vec::new();
223 ciborium::into_writer(cmd, &mut buf).unwrap();
224 buf
225 }
226
227 #[test]
228 fn encode_prepare_upgrade_round_trips_and_matches_backend_encoding() {
229 let payload = vec![1u8, 2, 3, 4];
230 let payload_sig = [0xDEu8; 64];
231 let nonce = [0x42u8; 32];
232
233 let bytes = encode_prepare_upgrade(&payload, &payload_sig, Some(sample_rekey()), nonce);
234
235 let expected = backend_style_encode(&ControlCommand::PrepareUpgrade {
237 payload: payload.clone(),
238 payload_signature: payload_sig.to_vec(),
239 rekey: Some(sample_rekey()),
240 nonce,
241 });
242 assert_eq!(bytes, expected);
243
244 let back: ControlCommand = ciborium::from_reader(bytes.as_slice()).unwrap();
246 match back {
247 ControlCommand::PrepareUpgrade {
248 payload: p,
249 payload_signature: ps,
250 rekey,
251 nonce: n,
252 } => {
253 assert_eq!(p, payload);
254 assert_eq!(ps, payload_sig.to_vec());
255 let rk = rekey.expect("rekey present");
256 assert_eq!(rk.new_public_key, vec![0xAB; 70]);
257 assert_eq!(rk.new_key_id, "arn:aws:kms:us-east-1:123:key/abc");
258 assert_eq!(n, nonce);
259 }
260 _ => panic!("wrong variant"),
261 }
262 }
263
264 #[test]
265 fn encode_prepare_upgrade_stateless_matches_backend_encoding() {
266 let bytes = encode_prepare_upgrade(&[0xAA], &[0xBB; 64], None, [1u8; 32]);
267 let expected = backend_style_encode(&ControlCommand::PrepareUpgrade {
268 payload: vec![0xAA],
269 payload_signature: vec![0xBB; 64],
270 rekey: None,
271 nonce: [1u8; 32],
272 });
273 assert_eq!(bytes, expected);
274 }
275
276 #[test]
277 fn encode_revoke_upgrade_round_trips_and_matches_backend_encoding() {
278 let payload = vec![0xCCu8; 8];
279 let payload_sig = [0xDDu8; 64];
280 let nonce = [0x99u8; 32];
281
282 for rollback in [true, false] {
283 let bytes = encode_revoke_upgrade(&payload, &payload_sig, rollback, nonce);
284 let expected = backend_style_encode(&ControlCommand::RevokeUpgrade {
285 payload: payload.clone(),
286 payload_signature: payload_sig.to_vec(),
287 rollback,
288 nonce,
289 });
290 assert_eq!(bytes, expected);
291
292 let back: ControlCommand = ciborium::from_reader(bytes.as_slice()).unwrap();
293 match back {
294 ControlCommand::RevokeUpgrade {
295 payload: p,
296 rollback: rb,
297 nonce: n,
298 ..
299 } => {
300 assert_eq!(p, payload);
301 assert_eq!(rb, rollback);
302 assert_eq!(n, nonce);
303 }
304 _ => panic!("wrong variant"),
305 }
306 }
307 }
308
309 #[test]
310 fn der_signature_to_raw_matches_direct_raw_encoding() {
311 let sk = SigningKey::from_bytes(&[7u8; 32].into()).unwrap();
312 let msg = b"custody der test vector";
313 let sig: Signature = sk.sign(msg);
314 let low = sig.normalize_s().unwrap_or(sig);
316
317 let raw = der_signature_to_raw(sig.to_der().as_bytes()).unwrap();
318 assert_eq!(raw, <[u8; 64]>::try_from(&low.to_bytes()[..]).unwrap());
319
320 let vk = VerifyingKey::from(&sk);
323 let parsed = Signature::from_slice(&raw).unwrap();
324 vk.verify(msg, &parsed).unwrap();
325 }
326
327 #[test]
328 fn der_signature_to_raw_normalizes_high_s() {
329 let sk = SigningKey::from_bytes(&[9u8; 32].into()).unwrap();
330 let msg = b"high-s normalization vector";
331 let sig: Signature = sk.sign(msg);
332 let low = sig.normalize_s().unwrap_or(sig);
333
334 let (r, s) = low.split_scalars();
337 let high_s = -*s.as_ref();
338 let high = Signature::from_scalars(r.to_bytes(), high_s.to_bytes()).unwrap();
339 assert!(high.normalize_s().is_some(), "twin must be high-S");
340
341 let raw = der_signature_to_raw(high.to_der().as_bytes()).unwrap();
342 assert_eq!(raw, <[u8; 64]>::try_from(&low.to_bytes()[..]).unwrap());
343
344 let vk = VerifyingKey::from(&sk);
345 let parsed = Signature::from_slice(&raw).unwrap();
346 vk.verify(msg, &parsed).unwrap();
347 }
348
349 #[test]
350 fn der_signature_to_raw_rejects_garbage() {
351 assert!(der_signature_to_raw(&[0u8; 64]).is_err());
352 assert!(der_signature_to_raw(b"not der").is_err());
353 assert!(der_signature_to_raw(&[]).is_err());
354 }
355
356 #[test]
357 fn confirm_prepare_response_serde_round_trip() {
358 let resp = ConfirmPrepareResponse {
359 payload: vec![1, 2, 3],
360 nonce: [0x11u8; 32],
361 rekey: Some(sample_rekey()),
362 valid_from: "2026-07-09T00:00:00Z".into(),
363 };
364 let json = serde_json::to_string(&resp).unwrap();
365 let back: ConfirmPrepareResponse = serde_json::from_str(&json).unwrap();
366 assert_eq!(back.payload, resp.payload);
367 assert_eq!(back.nonce, resp.nonce);
368 assert_eq!(back.valid_from, resp.valid_from);
369 let rk = back.rekey.as_ref().unwrap();
370 assert_eq!(rk.new_public_key, vec![0xAB; 70]);
371 assert_eq!(rk.new_key_id, "arn:aws:kms:us-east-1:123:key/abc");
372
373 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
375 assert!(v["payload"].is_string());
376 assert!(v["nonce"].is_string());
377 }
378
379 #[test]
383 fn rekey_survives_json_round_trip_byte_exactly() {
384 let payload = vec![5u8; 16];
385 let payload_sig = [0x77u8; 64];
386 let nonce = [0x33u8; 32];
387
388 let backend_cmd =
389 encode_prepare_upgrade(&payload, &payload_sig, Some(sample_rekey()), nonce);
390
391 let resp = ConfirmPrepareResponse {
392 payload,
393 nonce,
394 rekey: Some(sample_rekey()),
395 valid_from: "2026-07-09T00:00:00Z".into(),
396 };
397 let json = serde_json::to_string(&resp).unwrap();
398 let back: ConfirmPrepareResponse = serde_json::from_str(&json).unwrap();
399
400 let cli_cmd =
401 encode_prepare_upgrade(&back.payload, &payload_sig, back.rekey, back.nonce);
402 assert_eq!(cli_cmd, backend_cmd);
403 }
404
405 #[test]
406 fn confirm_submit_request_serde_round_trip() {
407 let req = ConfirmSubmitRequest {
408 command: vec![9, 8, 7],
409 envelope_signature: vec![0x55; 64],
410 };
411 let json = serde_json::to_string(&req).unwrap();
412 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
413 assert!(v["command"].is_string());
414 assert!(v["envelope_signature"].is_string());
415
416 let back: ConfirmSubmitRequest = serde_json::from_str(&json).unwrap();
417 assert_eq!(back.command, req.command);
418 assert_eq!(back.envelope_signature, req.envelope_signature);
419 }
420
421 #[test]
422 fn revoke_prepare_response_serde_round_trip() {
423 let resp = RevokePrepareResponse {
424 payload: vec![4, 5, 6],
425 nonce: [0x22u8; 32],
426 rollback: true,
427 };
428 let json = serde_json::to_string(&resp).unwrap();
429 let back: RevokePrepareResponse = serde_json::from_str(&json).unwrap();
430 assert_eq!(back.payload, resp.payload);
431 assert_eq!(back.nonce, resp.nonce);
432 assert!(back.rollback);
433 }
434
435 #[test]
436 fn nonce_deserialize_rejects_wrong_length() {
437 let json = format!(
439 r#"{{"payload":"AQID","nonce":"{}","rekey":null,"valid_from":"x"}}"#,
440 {
441 use base64::Engine as _;
442 base64::engine::general_purpose::STANDARD.encode([0u8; 31])
443 }
444 );
445 assert!(serde_json::from_str::<ConfirmPrepareResponse>(&json).is_err());
446 }
447}