Skip to main content

ant_node/upgrade/
signature.rs

1//! ML-DSA signature verification for binary upgrades.
2//!
3//! Provides quantum-resistant signature verification for auto-upgrade binaries
4//! using FIPS 204 ML-DSA-65.
5//!
6//! ## Memory Constraint
7//!
8//! ML-DSA-65 signature verification requires the complete message in memory.
9//! Unlike hash-based streaming verification (e.g., SHA-256), the ML-DSA algorithm
10//! must process the entire binary at once. This means:
11//!
12//! - **Memory usage**: The entire binary is loaded into RAM for verification
13//! - **Typical binaries**: 50-100MB for release builds, which is acceptable
14//!   for modern systems (minimum 512MB RAM recommended)
15//! - **No streaming**: Cannot verify signatures on files larger than available RAM
16//!
17//! This is an inherent property of FIPS 204 ML-DSA lattice-based signatures and
18//! is accepted as a reasonable trade-off for post-quantum security.
19
20use crate::error::{Error, Result};
21use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
22use std::fs;
23use std::path::Path;
24use tracing::debug;
25
26/// Signing context for domain separation (prevents cross-protocol attacks).
27pub const SIGNING_CONTEXT: &[u8] = b"ant-node-release-v1";
28
29/// ML-DSA-65 signature size in bytes.
30pub const SIGNATURE_SIZE: usize = 3309;
31
32/// ML-DSA-65 public key size in bytes.
33pub const PUBLIC_KEY_SIZE: usize = 1952;
34
35/// Embedded release signing public key (ML-DSA-65).
36///
37/// This key is used to verify signatures on released binaries.
38/// The corresponding private key is held by authorized release signers.
39/// Generated: 2026-03-24 22:03:22 UTC
40const RELEASE_SIGNING_KEY: &[u8] = &[
41    0x55, 0xed, 0xf3, 0xe1, 0xf8, 0xc6, 0xd3, 0xbf, 0x45, 0x89, 0x2c, 0x79, 0x6d, 0xdd, 0x23, 0x13,
42    0xbe, 0xad, 0xd9, 0xdf, 0x8e, 0x4f, 0xf4, 0x3c, 0xb8, 0x63, 0x28, 0x79, 0x6e, 0x34, 0x84, 0xd3,
43    0x7d, 0xfb, 0x1e, 0x10, 0x43, 0x43, 0x89, 0xe9, 0x21, 0xa1, 0x3c, 0xd4, 0xc0, 0x0b, 0x44, 0x5c,
44    0x23, 0x99, 0xbd, 0x20, 0xd7, 0xb0, 0xac, 0x58, 0x8e, 0xed, 0xa2, 0x5a, 0x71, 0xd6, 0x6c, 0xf8,
45    0x3f, 0xbe, 0x9c, 0x73, 0x6c, 0x3b, 0xc3, 0xb4, 0xc3, 0x77, 0x6d, 0x49, 0x92, 0x56, 0x6e, 0x84,
46    0x66, 0x28, 0x73, 0xe4, 0xc5, 0xbb, 0x32, 0xb4, 0x3a, 0x33, 0x6c, 0x33, 0x77, 0x54, 0xc8, 0xc0,
47    0x18, 0x71, 0x82, 0x6a, 0xf4, 0xf2, 0x10, 0xf4, 0xb7, 0xd1, 0x57, 0xc6, 0x1d, 0x36, 0xbb, 0x80,
48    0x2b, 0xaf, 0xbd, 0x52, 0xa6, 0xcc, 0xd5, 0xa1, 0x63, 0x34, 0x0c, 0xbb, 0xc3, 0x83, 0x4d, 0xfc,
49    0xeb, 0x8f, 0xf7, 0x28, 0x93, 0xcf, 0x67, 0x8c, 0x9c, 0xad, 0xc6, 0xf1, 0x0a, 0x95, 0x32, 0x7f,
50    0x2b, 0x2c, 0x6c, 0x65, 0x2f, 0x8f, 0x29, 0x5b, 0x47, 0x64, 0x43, 0x90, 0x33, 0x7f, 0x82, 0x3c,
51    0xe3, 0xec, 0x75, 0xdb, 0x3d, 0x6d, 0xe4, 0xc1, 0x42, 0x92, 0x80, 0x99, 0xdd, 0xd2, 0x10, 0xb4,
52    0x91, 0x3e, 0x7a, 0xf6, 0xed, 0xd8, 0xa5, 0x1f, 0xe4, 0x07, 0xf0, 0x7b, 0x7b, 0x5e, 0x8e, 0x12,
53    0xe6, 0x6e, 0x04, 0xe5, 0xa6, 0xf4, 0x24, 0x6a, 0x28, 0x18, 0x6b, 0x0a, 0x5a, 0x36, 0x5d, 0x46,
54    0xe2, 0x22, 0xd7, 0x67, 0x29, 0x7b, 0xb9, 0x8e, 0x88, 0xd9, 0x76, 0x58, 0xc9, 0x12, 0xdf, 0xb6,
55    0x97, 0x76, 0x21, 0xfa, 0xec, 0x6c, 0x67, 0xea, 0x63, 0xc8, 0x3b, 0xc3, 0xf6, 0x5c, 0xc0, 0x5e,
56    0xf8, 0x08, 0x82, 0x63, 0xfc, 0xf9, 0x24, 0x2a, 0xf5, 0x20, 0x8c, 0x3c, 0xb7, 0xa8, 0x84, 0x8d,
57    0x87, 0xd8, 0x03, 0x74, 0xc6, 0xb1, 0x32, 0x2b, 0xe8, 0xd9, 0x21, 0x86, 0xfb, 0x28, 0x13, 0x3b,
58    0xe9, 0x4d, 0x99, 0x8f, 0xa3, 0x5c, 0x8c, 0xe0, 0xc6, 0xf3, 0x9b, 0x25, 0xcc, 0x11, 0x48, 0x39,
59    0x50, 0x00, 0x3b, 0x0e, 0x1d, 0xb7, 0xd2, 0xca, 0x0f, 0x10, 0x08, 0xed, 0x65, 0xa6, 0x6f, 0xee,
60    0x66, 0xd7, 0x68, 0x48, 0x0d, 0x9a, 0x73, 0xdf, 0x1c, 0x00, 0xcc, 0xbe, 0x86, 0xa6, 0x4f, 0x15,
61    0x6b, 0xf3, 0x56, 0xbb, 0x0d, 0x93, 0xc3, 0x8d, 0xa8, 0x31, 0x84, 0x91, 0x13, 0x7b, 0x14, 0x10,
62    0x60, 0x59, 0x18, 0x16, 0x08, 0xfd, 0x2d, 0x4d, 0x0c, 0xb2, 0x8c, 0x94, 0x6f, 0x7d, 0x81, 0xe9,
63    0x43, 0x9f, 0xaa, 0x13, 0xc2, 0x03, 0xff, 0xe2, 0xa2, 0x06, 0x38, 0x3b, 0x9b, 0xce, 0x85, 0x23,
64    0x80, 0xea, 0xa9, 0x92, 0x2f, 0x29, 0x15, 0xba, 0x68, 0x33, 0x6a, 0x0d, 0xbe, 0x11, 0x87, 0x61,
65    0xf1, 0xd5, 0x59, 0x50, 0x70, 0x51, 0x91, 0x6d, 0x4a, 0x9e, 0x69, 0x7c, 0xdd, 0x23, 0x8f, 0xa6,
66    0x4e, 0xa6, 0x81, 0xe7, 0xbc, 0x62, 0x40, 0x7c, 0x0b, 0xc0, 0xa2, 0xe1, 0xf8, 0x66, 0xc1, 0x82,
67    0x4a, 0x72, 0xb3, 0xee, 0x45, 0x44, 0xc7, 0x7b, 0xb1, 0xce, 0xfd, 0x80, 0xa9, 0x20, 0x20, 0xe7,
68    0xad, 0xdf, 0x9a, 0xa2, 0x86, 0xf3, 0x64, 0x93, 0xb2, 0xc9, 0xb1, 0x56, 0x5d, 0x66, 0x67, 0x9c,
69    0x29, 0x23, 0x53, 0xc4, 0xc9, 0x85, 0xd1, 0xed, 0x84, 0x2b, 0xc4, 0xa9, 0x93, 0x1b, 0x8f, 0xd8,
70    0xff, 0x1e, 0x2d, 0xa8, 0x3c, 0x27, 0x6c, 0xa5, 0x23, 0x2a, 0x5e, 0xd0, 0xcf, 0x11, 0xe2, 0x03,
71    0x7b, 0xdb, 0xa0, 0x10, 0xde, 0x2e, 0xa9, 0x5b, 0x42, 0x77, 0xcf, 0xa4, 0xc8, 0xab, 0xbd, 0x67,
72    0xa8, 0x88, 0x7b, 0x51, 0xe4, 0x39, 0xeb, 0x3e, 0x4e, 0x8f, 0xf1, 0x18, 0x83, 0x43, 0x43, 0x7e,
73    0xd1, 0x58, 0x27, 0x60, 0x54, 0xd5, 0xa8, 0x30, 0x54, 0xa1, 0xd0, 0xdc, 0x94, 0xf0, 0x28, 0x8d,
74    0x9c, 0xe4, 0x94, 0x1a, 0xcc, 0x87, 0xf7, 0xce, 0x4c, 0x63, 0x0d, 0xd0, 0x6d, 0x45, 0x20, 0x90,
75    0x5c, 0x95, 0x3e, 0x3c, 0x2e, 0xd0, 0x35, 0x2c, 0x35, 0x49, 0x2c, 0x6a, 0x7e, 0x5d, 0x5d, 0xc8,
76    0xaf, 0xb2, 0x96, 0x0b, 0x88, 0xb9, 0x2b, 0x33, 0x82, 0xa4, 0xc3, 0x81, 0xd3, 0xb9, 0x7e, 0x17,
77    0xba, 0xa7, 0x57, 0x47, 0x4d, 0x33, 0x55, 0xeb, 0xa5, 0x51, 0xd7, 0xe7, 0xc9, 0x46, 0x95, 0x52,
78    0x72, 0xb3, 0xb7, 0x57, 0x9b, 0xf6, 0x57, 0xff, 0x9d, 0xc5, 0x93, 0x4b, 0x05, 0x48, 0x41, 0x81,
79    0x25, 0xfc, 0xd9, 0x9e, 0x56, 0x18, 0x36, 0x17, 0x74, 0x1f, 0x77, 0x41, 0x98, 0x3f, 0x9e, 0x20,
80    0x74, 0x3d, 0x4e, 0xe3, 0xa0, 0xa0, 0x6b, 0x64, 0x38, 0xd7, 0xf7, 0x8c, 0xef, 0x01, 0x68, 0x82,
81    0x53, 0x30, 0x0a, 0xb6, 0xd1, 0x81, 0xd1, 0x70, 0x3c, 0xea, 0x96, 0xd3, 0x89, 0x21, 0x0e, 0xbb,
82    0x5a, 0x80, 0x51, 0x99, 0xeb, 0xef, 0x06, 0x85, 0xae, 0x8a, 0xfd, 0x23, 0x5a, 0x91, 0x3c, 0xe1,
83    0x85, 0xed, 0xf5, 0xf0, 0x58, 0x88, 0xac, 0xd0, 0xe8, 0x83, 0xb6, 0x90, 0xf2, 0xe6, 0x76, 0x5c,
84    0xcf, 0xd8, 0xba, 0xba, 0x88, 0xec, 0x0b, 0xd3, 0x77, 0x66, 0x30, 0x6e, 0xcc, 0x84, 0x29, 0xd3,
85    0x5f, 0xdf, 0x6e, 0x5b, 0x7f, 0x9d, 0x97, 0xed, 0x25, 0x33, 0xc6, 0x93, 0x4e, 0x62, 0x62, 0x7e,
86    0x00, 0x35, 0x20, 0x68, 0x4e, 0x85, 0x4a, 0xc1, 0xe0, 0x20, 0x2f, 0x80, 0x11, 0x2f, 0x33, 0xac,
87    0xd3, 0x8d, 0x56, 0x71, 0xcc, 0xa6, 0xeb, 0xe1, 0x0e, 0x3f, 0xea, 0x46, 0x16, 0xc5, 0x5e, 0x84,
88    0xbd, 0xad, 0x7a, 0x7a, 0x4a, 0x8c, 0x73, 0x66, 0xaa, 0x3e, 0xdc, 0xbe, 0x65, 0xa6, 0x5a, 0xed,
89    0xc6, 0x32, 0xcb, 0xbb, 0x03, 0xb9, 0xfb, 0xf3, 0x7a, 0xdc, 0x71, 0x7b, 0x46, 0x86, 0x4d, 0x55,
90    0xeb, 0x70, 0x05, 0xc9, 0x82, 0x03, 0xdf, 0x22, 0xa4, 0xfd, 0x25, 0xb1, 0x37, 0x3b, 0x90, 0x80,
91    0x35, 0x45, 0xab, 0x66, 0x0e, 0x4c, 0xcf, 0x5f, 0x69, 0x2f, 0x7b, 0x09, 0x7d, 0x6a, 0xb1, 0x91,
92    0x0d, 0x7e, 0xb4, 0xd4, 0xcb, 0x5f, 0xa3, 0xf3, 0x00, 0x73, 0x03, 0xbf, 0xc9, 0x6e, 0x9c, 0x48,
93    0x9e, 0x1c, 0x3b, 0xb0, 0x1f, 0xab, 0xb6, 0xea, 0x72, 0x04, 0x9d, 0xec, 0x6c, 0xc9, 0xd2, 0xed,
94    0x6f, 0x72, 0x19, 0x5c, 0x3a, 0xfc, 0x57, 0x68, 0xf6, 0x05, 0x88, 0xd8, 0xe6, 0x38, 0xd4, 0xe0,
95    0x9c, 0xed, 0xa7, 0x38, 0xa9, 0xab, 0xd4, 0x79, 0xf0, 0x3e, 0x39, 0x32, 0xa3, 0x6b, 0x2a, 0x49,
96    0x53, 0x12, 0xaf, 0x7f, 0x6d, 0x26, 0xf6, 0x16, 0x8c, 0x9d, 0xac, 0x0d, 0xf7, 0x33, 0xf4, 0x69,
97    0xc9, 0x47, 0xec, 0xba, 0xac, 0x95, 0x7e, 0x7d, 0x41, 0xc0, 0xca, 0xce, 0xd5, 0xf8, 0x9e, 0x49,
98    0xe0, 0x4d, 0xaa, 0x13, 0xea, 0x05, 0x3a, 0xf2, 0x71, 0x3b, 0xad, 0x8b, 0x16, 0xd7, 0x12, 0x07,
99    0xea, 0xf6, 0x81, 0x09, 0x3f, 0x04, 0x20, 0x2f, 0xd5, 0x78, 0xd8, 0xc4, 0xee, 0xca, 0x9e, 0x43,
100    0xe1, 0x5b, 0xe4, 0x21, 0xaf, 0xeb, 0xbf, 0xf8, 0x8c, 0x52, 0x1f, 0x3f, 0xb8, 0x8f, 0x3c, 0xfa,
101    0x3c, 0x94, 0x81, 0x59, 0x53, 0x21, 0xd6, 0xf1, 0x3a, 0x9a, 0x7b, 0xd8, 0x18, 0xe9, 0xbd, 0x41,
102    0xc4, 0x4a, 0x5c, 0x8a, 0x21, 0x2e, 0xda, 0x70, 0x32, 0xb7, 0xef, 0x49, 0x88, 0x38, 0xe3, 0x8d,
103    0xd9, 0x53, 0xd1, 0xc2, 0x9c, 0xec, 0xf6, 0x76, 0x75, 0x0a, 0x31, 0x37, 0xba, 0xd2, 0xaa, 0x37,
104    0xce, 0x30, 0x7a, 0x2b, 0xdb, 0xce, 0x44, 0x86, 0x6c, 0x20, 0x1a, 0x6c, 0xae, 0x0a, 0x20, 0xbb,
105    0xfa, 0x6b, 0x04, 0x11, 0xdd, 0xfd, 0x89, 0x7a, 0xaf, 0x1f, 0x63, 0x85, 0x75, 0x78, 0x62, 0x77,
106    0xa0, 0x13, 0x8e, 0x4c, 0x5b, 0x11, 0x06, 0xcf, 0x97, 0x28, 0x2d, 0x9a, 0xd1, 0x5c, 0xcd, 0x3e,
107    0x0a, 0xc0, 0x42, 0x93, 0x60, 0xe2, 0x37, 0x1b, 0xf5, 0x68, 0x72, 0xd6, 0x8b, 0xd1, 0x78, 0xe4,
108    0x1a, 0xa5, 0xbc, 0x7f, 0xd2, 0x6e, 0xfd, 0x3e, 0xdd, 0xca, 0x6f, 0x32, 0x7b, 0x19, 0x39, 0x48,
109    0x88, 0x31, 0x10, 0x7f, 0xa6, 0x4d, 0xc9, 0xe3, 0x1d, 0x40, 0xe1, 0x11, 0xfc, 0xbc, 0x32, 0xa9,
110    0x06, 0xde, 0x17, 0x29, 0x14, 0x50, 0x83, 0xdb, 0x54, 0x2c, 0x9e, 0x75, 0x4e, 0xf7, 0xee, 0xc4,
111    0x8d, 0xc2, 0xf3, 0xf8, 0x03, 0x02, 0x0e, 0x4e, 0xc4, 0x42, 0xef, 0x5b, 0x3b, 0x37, 0x52, 0x2e,
112    0x51, 0xf3, 0x72, 0x18, 0x32, 0x48, 0x42, 0xbe, 0xdf, 0xe2, 0x46, 0x49, 0x17, 0x77, 0xe4, 0x4e,
113    0x5f, 0xda, 0x50, 0xda, 0xce, 0x30, 0x65, 0x3a, 0xf9, 0xef, 0xd4, 0x8b, 0x91, 0x88, 0xbb, 0xb9,
114    0x69, 0x65, 0xaa, 0x3b, 0xe1, 0xd5, 0xf8, 0x0a, 0x2a, 0x8a, 0xc9, 0x67, 0x54, 0xd8, 0x54, 0x55,
115    0x3e, 0xac, 0x9e, 0x9d, 0x73, 0x6a, 0x1f, 0x21, 0xe1, 0x2b, 0xaa, 0x87, 0x16, 0x40, 0xfc, 0x37,
116    0xd8, 0xa9, 0xbc, 0x39, 0x72, 0x78, 0xfc, 0x9d, 0x6d, 0xe2, 0xd8, 0x5d, 0x30, 0xbb, 0xd4, 0x9b,
117    0xbd, 0xf3, 0xeb, 0x64, 0x78, 0x4a, 0x63, 0x33, 0xe9, 0x95, 0x9c, 0xac, 0x94, 0xc1, 0xf8, 0xd9,
118    0x2c, 0x25, 0x46, 0x92, 0x1a, 0xa5, 0x77, 0xca, 0x02, 0xaf, 0x97, 0x49, 0x24, 0x2f, 0xe6, 0x77,
119    0x3a, 0x3d, 0x35, 0x05, 0x08, 0xd9, 0x90, 0x74, 0xb2, 0x28, 0x6e, 0xbd, 0x34, 0x85, 0xd5, 0x47,
120    0x88, 0x5d, 0x36, 0xe8, 0x8c, 0xd1, 0x9b, 0x5d, 0x49, 0xa8, 0x94, 0x46, 0x22, 0xbd, 0x67, 0x19,
121    0x77, 0x8f, 0xb7, 0xda, 0x17, 0x68, 0x49, 0xf0, 0xf5, 0x6d, 0x2a, 0xe4, 0x9b, 0x4c, 0x07, 0xac,
122    0x91, 0xa6, 0x2d, 0x41, 0x6f, 0xf6, 0x40, 0xa7, 0xe9, 0xc4, 0x3e, 0x7c, 0xc3, 0xb6, 0x8b, 0xf5,
123    0x0a, 0x47, 0x19, 0xd4, 0x86, 0x16, 0x40, 0x02, 0x5a, 0x9b, 0x6f, 0xe0, 0x83, 0x9b, 0x84, 0x61,
124    0x28, 0x35, 0xdb, 0xa6, 0x22, 0xae, 0x67, 0x0c, 0x72, 0x5f, 0xd0, 0xdc, 0x9c, 0x8b, 0xca, 0xa5,
125    0xc7, 0xed, 0x5c, 0xe0, 0xba, 0xe7, 0x57, 0xbb, 0xf3, 0xc2, 0xa4, 0x21, 0x16, 0x11, 0xe9, 0x5e,
126    0x47, 0xdd, 0xb5, 0x88, 0x9c, 0x6a, 0x2c, 0xcc, 0x6b, 0x2c, 0x19, 0xbc, 0x52, 0x3f, 0xf5, 0x51,
127    0x15, 0xcd, 0xae, 0x61, 0x1f, 0xca, 0x16, 0xfb, 0xd8, 0xd7, 0x20, 0x2f, 0xe4, 0xb9, 0xd4, 0x9a,
128    0x8b, 0xf7, 0xa5, 0x03, 0x5d, 0x6b, 0xde, 0x57, 0x69, 0xef, 0x50, 0xff, 0xed, 0x2d, 0x41, 0x4f,
129    0x5c, 0x87, 0x7a, 0x04, 0xce, 0x91, 0xf0, 0x23, 0x14, 0x20, 0x54, 0xae, 0x25, 0x6a, 0xea, 0x71,
130    0xd2, 0x5c, 0x26, 0x3b, 0xc9, 0x75, 0xe4, 0x68, 0x92, 0x66, 0x63, 0x03, 0xa0, 0xf6, 0x9c, 0x46,
131    0x48, 0x89, 0xe0, 0xad, 0x10, 0x4f, 0x35, 0x6f, 0x68, 0xb9, 0x63, 0xd0, 0x9c, 0xa6, 0xa6, 0x0e,
132    0x93, 0x95, 0x63, 0xda, 0xea, 0x26, 0x27, 0xab, 0x10, 0x67, 0x6d, 0xe8, 0xc8, 0x4e, 0xc9, 0x9b,
133    0x53, 0x96, 0xa8, 0x6f, 0x77, 0x71, 0x37, 0xef, 0xf6, 0x52, 0x51, 0x94, 0x7a, 0x3b, 0x8e, 0xf2,
134    0x4c, 0x14, 0x21, 0x59, 0xc5, 0xe9, 0x85, 0x1a, 0xba, 0xf2, 0x98, 0xaf, 0x80, 0x01, 0xc4, 0x57,
135    0x78, 0xde, 0x44, 0xa9, 0x0e, 0xc5, 0x31, 0x88, 0xe0, 0x65, 0x56, 0x8c, 0x16, 0xdb, 0x80, 0x89,
136    0x63, 0xd5, 0xc2, 0x91, 0x7d, 0x90, 0x89, 0x7b, 0xfa, 0xa0, 0x2a, 0x55, 0xcc, 0x62, 0x20, 0xcb,
137    0xcf, 0x89, 0x28, 0x8b, 0xa5, 0x70, 0x55, 0x69, 0x76, 0x30, 0xb0, 0xf0, 0x7e, 0x30, 0x64, 0x4c,
138    0x7b, 0x4b, 0x41, 0xcf, 0x6f, 0x28, 0x8b, 0x99, 0x5f, 0x8a, 0xab, 0xf5, 0xfb, 0x45, 0xb6, 0xcd,
139    0xca, 0xc9, 0x87, 0xb0, 0x83, 0x3e, 0xe3, 0x18, 0xce, 0xab, 0x6b, 0xd7, 0x57, 0x84, 0xd6, 0x43,
140    0x1c, 0x10, 0xc2, 0xf2, 0x8b, 0xb4, 0xd2, 0xab, 0xd1, 0xca, 0x73, 0xc6, 0xc0, 0x72, 0xa7, 0x69,
141    0x74, 0x02, 0x73, 0xf6, 0xa3, 0xe3, 0x9b, 0x6d, 0xc9, 0x01, 0x16, 0x95, 0x78, 0x9e, 0x17, 0x73,
142    0x39, 0xca, 0x0a, 0xab, 0x03, 0x7c, 0x3b, 0xe9, 0x41, 0x69, 0x71, 0x47, 0xba, 0xc4, 0xc1, 0x34,
143    0x9f, 0x47, 0x2b, 0xf2, 0x8e, 0x01, 0xf0, 0xe2, 0x86, 0x18, 0xb1, 0x80, 0x46, 0x06, 0x61, 0x42,
144    0xd8, 0x25, 0x41, 0xe1, 0xd7, 0x17, 0x8b, 0xa3, 0x1f, 0x64, 0x91, 0x1f, 0x28, 0x82, 0x1a, 0x63,
145    0x11, 0x68, 0xa7, 0x0f, 0x9a, 0x8c, 0xaf, 0x61, 0x18, 0x98, 0x31, 0x3a, 0x95, 0xe4, 0xf4, 0x73,
146    0x8b, 0xf5, 0xb2, 0x1c, 0x19, 0xcb, 0x22, 0x77, 0x23, 0x07, 0x85, 0x80, 0xc9, 0x1c, 0x74, 0xee,
147    0xbf, 0x97, 0x97, 0x60, 0x17, 0x29, 0x2a, 0x4c, 0x50, 0x64, 0x7e, 0xa0, 0x99, 0x13, 0x86, 0xa6,
148    0x89, 0x3c, 0xd6, 0xd8, 0x9c, 0xef, 0x24, 0xe6, 0x92, 0xe9, 0x58, 0xc6, 0x38, 0x61, 0x86, 0x39,
149    0xcf, 0xbe, 0x70, 0xf6, 0x5a, 0x69, 0x80, 0x6e, 0x80, 0xcb, 0x14, 0x6f, 0x9a, 0x26, 0x30, 0x28,
150    0x3d, 0x32, 0x07, 0x41, 0x24, 0x9c, 0x84, 0xba, 0x4a, 0x67, 0x5f, 0xf0, 0x17, 0x28, 0x23, 0xfe,
151    0x39, 0x39, 0x80, 0xb5, 0xac, 0x28, 0x53, 0x97, 0xdd, 0x0d, 0x5b, 0x55, 0xd4, 0x22, 0x81, 0x81,
152    0x4d, 0xb4, 0x8b, 0x2b, 0x33, 0xad, 0x62, 0xd5, 0x8d, 0xfe, 0x39, 0xd4, 0xd8, 0xf1, 0x5d, 0x0f,
153    0x19, 0x64, 0xbf, 0xac, 0x91, 0xf1, 0x92, 0x3c, 0x7f, 0x3a, 0xb8, 0xa6, 0xb5, 0xd7, 0x3a, 0x36,
154    0xc8, 0x9c, 0x65, 0x1b, 0xa6, 0x2a, 0x98, 0x15, 0x96, 0x23, 0xd4, 0xe3, 0xa6, 0x07, 0x23, 0xa9,
155    0xce, 0x16, 0x50, 0xb1, 0x97, 0xce, 0x69, 0x00, 0x68, 0x1a, 0xf8, 0x71, 0x88, 0x55, 0x5e, 0xf1,
156    0xd9, 0x81, 0x64, 0x1c, 0x95, 0x2b, 0x68, 0xf9, 0xbe, 0x78, 0x9a, 0xbb, 0xf9, 0x2c, 0x7f, 0x6e,
157    0xed, 0x66, 0xfd, 0x44, 0xaa, 0xfe, 0x96, 0x84, 0xde, 0xbc, 0x47, 0x78, 0xc4, 0x90, 0xb8, 0xe4,
158    0xde, 0xb8, 0x3f, 0xaf, 0xc5, 0x05, 0xe6, 0x6e, 0x62, 0x4c, 0x43, 0x07, 0xbf, 0x06, 0x99, 0xf3,
159    0x18, 0x38, 0x26, 0x3f, 0x87, 0xd7, 0x04, 0x0e, 0xa0, 0x0d, 0xbd, 0xf8, 0xda, 0x50, 0x24, 0xe9,
160    0x46, 0x28, 0xbe, 0x70, 0x7f, 0x79, 0x25, 0xd1, 0x1a, 0xe6, 0x5e, 0x74, 0xe5, 0x2f, 0x5d, 0xd2,
161    0xc2, 0x60, 0xe9, 0x6e, 0x15, 0x4c, 0x70, 0x40, 0x99, 0x88, 0xe0, 0x93, 0xfe, 0x36, 0x1e, 0x64,
162    0x14, 0xa5, 0xcf, 0x35, 0xf2, 0xd2, 0xab, 0x8d, 0xf4, 0x1d, 0x29, 0xbd, 0xac, 0x08, 0xaf, 0xd6,
163];
164
165/// Verify the ML-DSA signature on a binary file using the embedded release key.
166///
167/// # Arguments
168///
169/// * `binary_path` - Path to the binary to verify
170/// * `signature` - The ML-DSA-65 signature bytes
171///
172/// # Errors
173///
174/// Returns an error if:
175/// - The binary file cannot be read
176/// - The signature format is invalid
177/// - The signature verification fails
178pub fn verify_binary_signature(binary_path: &Path, signature: &[u8]) -> Result<()> {
179    let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY)
180        .map_err(|e| Error::Crypto(format!("Invalid release key: {e}")))?;
181
182    verify_binary_signature_with_key(binary_path, signature, &public_key)
183}
184
185/// Verify signature with an explicit public key.
186///
187/// This function is useful for testing and for cases where the public key
188/// is provided externally rather than embedded in the binary.
189///
190/// # Arguments
191///
192/// * `binary_path` - Path to the binary to verify
193/// * `signature` - The ML-DSA-65 signature bytes
194/// * `public_key` - The public key to verify against
195///
196/// # Errors
197///
198/// Returns an error if:
199/// - The binary file cannot be read
200/// - The signature has an invalid size
201/// - The signature format is invalid
202/// - The signature verification fails
203pub fn verify_binary_signature_with_key(
204    binary_path: &Path,
205    signature: &[u8],
206    public_key: &MlDsaPublicKey,
207) -> Result<()> {
208    debug!("Verifying signature for: {}", binary_path.display());
209
210    // Read binary content
211    let binary_content = fs::read(binary_path).map_err(|e| {
212        Error::Crypto(format!(
213            "Failed to read binary '{}': {e}",
214            binary_path.display()
215        ))
216    })?;
217
218    // Validate signature size
219    if signature.len() != SIGNATURE_SIZE {
220        return Err(Error::Crypto(format!(
221            "Invalid signature size: expected {SIGNATURE_SIZE}, got {}",
222            signature.len()
223        )));
224    }
225
226    // Parse signature
227    let sig = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, signature)
228        .map_err(|e| Error::Crypto(format!("Invalid signature format: {e}")))?;
229
230    // Verify with context
231    let dsa = ml_dsa_65();
232    let valid = dsa
233        .verify_with_context(public_key, &binary_content, &sig, SIGNING_CONTEXT)
234        .map_err(|e| Error::Crypto(format!("Signature verification error: {e}")))?;
235
236    if valid {
237        debug!("Signature verified successfully");
238        Ok(())
239    } else {
240        Err(Error::Crypto(
241            "Signature verification failed: invalid signature".to_string(),
242        ))
243    }
244}
245
246/// Verify a signature from a detached .sig file.
247///
248/// # Arguments
249///
250/// * `binary_path` - Path to the binary to verify
251/// * `signature_path` - Path to the detached signature file
252///
253/// # Errors
254///
255/// Returns an error if the signature file cannot be read or verification fails.
256pub fn verify_from_file(binary_path: &Path, signature_path: &Path) -> Result<()> {
257    debug!(
258        "Verifying {} with signature from {}",
259        binary_path.display(),
260        signature_path.display()
261    );
262
263    let signature = fs::read(signature_path)?;
264    verify_binary_signature(binary_path, &signature)
265}
266
267/// Verify from file with explicit key.
268///
269/// # Arguments
270///
271/// * `binary_path` - Path to the binary to verify
272/// * `signature_path` - Path to the detached signature file
273/// * `public_key` - The public key to verify against
274///
275/// # Errors
276///
277/// Returns an error if the signature file cannot be read or verification fails.
278pub fn verify_from_file_with_key(
279    binary_path: &Path,
280    signature_path: &Path,
281    public_key: &MlDsaPublicKey,
282) -> Result<()> {
283    debug!(
284        "Verifying {} with signature from {}",
285        binary_path.display(),
286        signature_path.display()
287    );
288
289    let signature = fs::read(signature_path)?;
290    verify_binary_signature_with_key(binary_path, &signature, public_key)
291}
292
293#[cfg(test)]
294#[allow(
295    clippy::unwrap_used,
296    clippy::expect_used,
297    clippy::cast_possible_truncation,
298    clippy::cast_sign_loss
299)]
300mod tests {
301    use super::*;
302    use saorsa_pqc::api::sig::ml_dsa_65;
303    use std::io::Write;
304    use tempfile::NamedTempFile;
305
306    /// Test 1: Valid signature verification
307    #[test]
308    fn test_verify_valid_signature() {
309        let dsa = ml_dsa_65();
310        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
311        let binary_content = b"test binary content for signing";
312
313        let mut file = NamedTempFile::new().unwrap();
314        file.write_all(binary_content).unwrap();
315
316        let signature = dsa
317            .sign_with_context(&secret_key, binary_content, SIGNING_CONTEXT)
318            .unwrap();
319
320        let result =
321            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &public_key);
322        assert!(result.is_ok(), "Valid signature should verify: {result:?}");
323    }
324
325    /// Test 2: Invalid signature rejected
326    #[test]
327    fn test_reject_invalid_signature() {
328        let dsa = ml_dsa_65();
329        let (public_key, _) = dsa.generate_keypair().unwrap();
330        let binary_content = b"test binary content";
331
332        let mut file = NamedTempFile::new().unwrap();
333        file.write_all(binary_content).unwrap();
334
335        // All-zero signature is invalid
336        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
337
338        let result = verify_binary_signature_with_key(file.path(), &invalid_sig, &public_key);
339        assert!(result.is_err(), "Invalid signature should be rejected");
340    }
341
342    /// Test 3: Wrong key rejected
343    #[test]
344    fn test_reject_wrong_key() {
345        let dsa = ml_dsa_65();
346        let (_, secret_key) = dsa.generate_keypair().unwrap();
347        let (wrong_key, _) = dsa.generate_keypair().unwrap();
348        let binary_content = b"test binary content";
349
350        let mut file = NamedTempFile::new().unwrap();
351        file.write_all(binary_content).unwrap();
352
353        let signature = dsa
354            .sign_with_context(&secret_key, binary_content, SIGNING_CONTEXT)
355            .unwrap();
356
357        let result =
358            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &wrong_key);
359        assert!(result.is_err(), "Wrong key should fail verification");
360    }
361
362    /// Test 4: Modified binary rejected
363    #[test]
364    fn test_reject_modified_binary() {
365        let dsa = ml_dsa_65();
366        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
367        let original_content = b"original binary content";
368
369        // Sign the original content
370        let signature = dsa
371            .sign_with_context(&secret_key, original_content, SIGNING_CONTEXT)
372            .unwrap();
373
374        // Write modified content to file
375        let mut file = NamedTempFile::new().unwrap();
376        file.write_all(b"MODIFIED binary content").unwrap();
377
378        let result =
379            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &public_key);
380        assert!(result.is_err(), "Modified binary should fail verification");
381    }
382
383    /// Test 5: Malformed signature rejected
384    #[test]
385    fn test_reject_malformed_signature() {
386        let dsa = ml_dsa_65();
387        let (public_key, _) = dsa.generate_keypair().unwrap();
388        let binary_content = b"test content";
389
390        let mut file = NamedTempFile::new().unwrap();
391        file.write_all(binary_content).unwrap();
392
393        // Too short signature
394        let short_sig = vec![0u8; 100];
395
396        let result = verify_binary_signature_with_key(file.path(), &short_sig, &public_key);
397        assert!(result.is_err(), "Malformed signature should be rejected");
398        assert!(
399            result
400                .unwrap_err()
401                .to_string()
402                .contains("Invalid signature size"),
403            "Error should mention invalid size"
404        );
405    }
406
407    /// Test 6: Empty file handling
408    #[test]
409    fn test_empty_file() {
410        let dsa = ml_dsa_65();
411        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
412        let empty_content: &[u8] = b"";
413
414        let mut file = NamedTempFile::new().unwrap();
415        file.write_all(empty_content).unwrap();
416
417        let signature = dsa
418            .sign_with_context(&secret_key, empty_content, SIGNING_CONTEXT)
419            .unwrap();
420
421        let result =
422            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &public_key);
423        assert!(result.is_ok(), "Empty file should verify: {result:?}");
424    }
425
426    /// Test 7: Non-existent file
427    #[test]
428    fn test_nonexistent_file() {
429        let dsa = ml_dsa_65();
430        let (public_key, _) = dsa.generate_keypair().unwrap();
431        let path = Path::new("/nonexistent/path/to/binary");
432        let sig = vec![0u8; SIGNATURE_SIZE];
433
434        let result = verify_binary_signature_with_key(path, &sig, &public_key);
435        assert!(result.is_err(), "Non-existent file should fail");
436    }
437
438    /// Test 8: Context binding (cross-protocol attack prevention)
439    #[test]
440    fn test_wrong_context_rejected() {
441        let dsa = ml_dsa_65();
442        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
443        let content = b"binary content";
444
445        let mut file = NamedTempFile::new().unwrap();
446        file.write_all(content).unwrap();
447
448        // Sign with wrong context
449        let signature = dsa
450            .sign_with_context(&secret_key, content, b"wrong-context-string")
451            .unwrap();
452
453        // Verify with correct context should fail
454        let result =
455            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &public_key);
456        assert!(
457            result.is_err(),
458            "Wrong context should fail verification: {result:?}"
459        );
460    }
461
462    /// Test 9: Verify from detached .sig file
463    #[test]
464    fn test_verify_from_sig_file() {
465        let dsa = ml_dsa_65();
466        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
467        let content = b"binary content for sig file test";
468
469        let mut binary_file = NamedTempFile::new().unwrap();
470        binary_file.write_all(content).unwrap();
471
472        let signature = dsa
473            .sign_with_context(&secret_key, content, SIGNING_CONTEXT)
474            .unwrap();
475
476        let mut sig_file = NamedTempFile::new().unwrap();
477        sig_file.write_all(&signature.to_bytes()).unwrap();
478
479        let result = verify_from_file_with_key(binary_file.path(), sig_file.path(), &public_key);
480        assert!(
481            result.is_ok(),
482            "Detached sig file verification should succeed: {result:?}"
483        );
484    }
485
486    /// Test 10: Large binary file
487    #[test]
488    fn test_large_binary() {
489        let dsa = ml_dsa_65();
490        let (public_key, secret_key) = dsa.generate_keypair().unwrap();
491
492        // Create 1MB of test content
493        let large_content: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
494
495        let mut file = NamedTempFile::new().unwrap();
496        file.write_all(&large_content).unwrap();
497
498        let signature = dsa
499            .sign_with_context(&secret_key, &large_content, SIGNING_CONTEXT)
500            .unwrap();
501
502        let result =
503            verify_binary_signature_with_key(file.path(), &signature.to_bytes(), &public_key);
504        assert!(result.is_ok(), "Large binary should verify: {result:?}");
505    }
506
507    /// Test: Embedded release key is properly configured
508    #[test]
509    fn test_release_key_configured() {
510        // Verify the embedded key has correct size
511        assert_eq!(
512            RELEASE_SIGNING_KEY.len(),
513            PUBLIC_KEY_SIZE,
514            "Embedded release key should be {PUBLIC_KEY_SIZE} bytes"
515        );
516
517        // Verify the key can be parsed
518        let result = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, RELEASE_SIGNING_KEY);
519        assert!(
520            result.is_ok(),
521            "Embedded release key should be valid ML-DSA-65 public key"
522        );
523    }
524
525    /// Test: Invalid signature rejected with embedded key
526    #[test]
527    fn test_embedded_key_rejects_invalid_signature() {
528        let content = b"test binary content";
529
530        let mut file = NamedTempFile::new().unwrap();
531        file.write_all(content).unwrap();
532
533        // All-zero signature should be rejected
534        let invalid_sig = vec![0u8; SIGNATURE_SIZE];
535
536        let result = verify_binary_signature(file.path(), &invalid_sig);
537        assert!(result.is_err(), "Invalid signature should be rejected");
538    }
539}