Skip to main content

punch_types/
signing.rs

1//! Ed25519 manifest signing and verification.
2//!
3//! Every manifest that enters the ring can be cryptographically signed to
4//! guarantee authenticity and integrity. A fighter's identity is bound to
5//! an Ed25519 keypair — the signing key stays in the corner, while the
6//! verifying key can be distributed to anyone who needs to validate a
7//! manifest before it lands.
8
9use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
10use serde::{Deserialize, Serialize};
11
12// ---------------------------------------------------------------------------
13// Error type
14// ---------------------------------------------------------------------------
15
16/// Errors that can occur during signing or verification.
17#[derive(Debug, thiserror::Error)]
18pub enum SigningError {
19    /// The hex-encoded key could not be decoded.
20    #[error("invalid hex encoding: {0}")]
21    HexDecode(String),
22
23    /// The key bytes have an invalid length or format.
24    #[error("invalid key format: {0}")]
25    InvalidKey(String),
26
27    /// The signature bytes have an invalid length or format.
28    #[error("invalid signature format: {0}")]
29    InvalidSignature(String),
30
31    /// Signature verification failed.
32    #[error("signature verification failed")]
33    VerificationFailed,
34}
35
36// ---------------------------------------------------------------------------
37// SigningKeyPair
38// ---------------------------------------------------------------------------
39
40/// An Ed25519 signing keypair — the fighter's secret identity in the ring.
41///
42/// Wraps `ed25519_dalek::SigningKey` and provides convenient methods for
43/// signing manifests and serializing keys to hex strings.
44pub struct SigningKeyPair {
45    inner: SigningKey,
46}
47
48impl std::fmt::Debug for SigningKeyPair {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("SigningKeyPair")
51            .field("public_key", &self.verifying_key_hex())
52            .finish()
53    }
54}
55
56impl SigningKeyPair {
57    /// Create a `SigningKeyPair` from raw secret key bytes (32 bytes).
58    pub fn from_bytes(bytes: &[u8; 32]) -> Self {
59        Self {
60            inner: SigningKey::from_bytes(bytes),
61        }
62    }
63
64    /// Reconstruct a `SigningKeyPair` from a hex-encoded secret key.
65    pub fn from_hex(hex: &str) -> Result<Self, SigningError> {
66        let bytes = hex_decode(hex)?;
67        let arr: [u8; 32] = bytes
68            .try_into()
69            .map_err(|_| SigningError::InvalidKey("secret key must be 32 bytes".into()))?;
70        Ok(Self::from_bytes(&arr))
71    }
72
73    /// Return the secret key as a hex-encoded string.
74    pub fn secret_key_hex(&self) -> String {
75        hex_encode(self.inner.as_bytes())
76    }
77
78    /// Return the corresponding verifying (public) key.
79    pub fn verifying_key(&self) -> VerifyingKey {
80        self.inner.verifying_key()
81    }
82
83    /// Return the verifying key as a hex-encoded string.
84    pub fn verifying_key_hex(&self) -> String {
85        hex_encode(self.verifying_key().as_bytes())
86    }
87
88    /// Sign arbitrary bytes and return the signature.
89    pub fn sign(&self, message: &[u8]) -> Signature {
90        self.inner.sign(message)
91    }
92}
93
94// ---------------------------------------------------------------------------
95// Key generation
96// ---------------------------------------------------------------------------
97
98/// Generate a fresh Ed25519 keypair using OS-level randomness.
99///
100/// Returns the signing keypair and the corresponding verifying key.
101pub fn generate_keypair() -> (SigningKeyPair, VerifyingKey) {
102    let mut csprng = rand::rngs::OsRng;
103    let signing_key = SigningKey::generate(&mut csprng);
104    let verifying_key = signing_key.verifying_key();
105    (SigningKeyPair { inner: signing_key }, verifying_key)
106}
107
108// ---------------------------------------------------------------------------
109// Manifest signing helpers
110// ---------------------------------------------------------------------------
111
112/// Sign manifest bytes and return a hex-encoded 64-byte signature.
113pub fn sign_manifest(keypair: &SigningKeyPair, manifest_bytes: &[u8]) -> String {
114    let sig = keypair.sign(manifest_bytes);
115    hex_encode(&sig.to_bytes())
116}
117
118/// Verify a hex-encoded signature against manifest bytes and a verifying key.
119pub fn verify_manifest(
120    verifying_key: &VerifyingKey,
121    manifest_bytes: &[u8],
122    signature_hex: &str,
123) -> Result<bool, SigningError> {
124    let sig_bytes = hex_decode(signature_hex)?;
125    let sig_arr: [u8; 64] = sig_bytes
126        .try_into()
127        .map_err(|_| SigningError::InvalidSignature("signature must be 64 bytes".into()))?;
128    let signature = Signature::from_bytes(&sig_arr);
129    Ok(verifying_key.verify(manifest_bytes, &signature).is_ok())
130}
131
132// ---------------------------------------------------------------------------
133// SignedManifest
134// ---------------------------------------------------------------------------
135
136/// A manifest bundled with its signature and the signer's public key.
137///
138/// Everything needed to verify authenticity in a single struct — ready to
139/// be serialized and transmitted across the ring.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SignedManifest {
142    /// The raw manifest bytes.
143    pub manifest: Vec<u8>,
144    /// Hex-encoded Ed25519 signature (128 hex chars = 64 bytes).
145    pub signature: String,
146    /// Hex-encoded Ed25519 public key (64 hex chars = 32 bytes).
147    pub public_key: String,
148}
149
150/// Sign manifest bytes and wrap them into a `SignedManifest`.
151pub fn sign_and_wrap(keypair: &SigningKeyPair, manifest: Vec<u8>) -> SignedManifest {
152    let signature = sign_manifest(keypair, &manifest);
153    let public_key = keypair.verifying_key_hex();
154    SignedManifest {
155        manifest,
156        signature,
157        public_key,
158    }
159}
160
161/// Verify a `SignedManifest` — reconstruct the public key from the embedded
162/// hex string and check the signature against the manifest bytes.
163pub fn verify_signed_manifest(signed: &SignedManifest) -> Result<bool, SigningError> {
164    let pk_bytes = hex_decode(&signed.public_key)?;
165    let pk_arr: [u8; 32] = pk_bytes
166        .try_into()
167        .map_err(|_| SigningError::InvalidKey("public key must be 32 bytes".into()))?;
168    let verifying_key = VerifyingKey::from_bytes(&pk_arr)
169        .map_err(|e| SigningError::InvalidKey(format!("invalid public key: {}", e)))?;
170    verify_manifest(&verifying_key, &signed.manifest, &signed.signature)
171}
172
173// ---------------------------------------------------------------------------
174// Verifying key from hex
175// ---------------------------------------------------------------------------
176
177/// Reconstruct a `VerifyingKey` from a hex-encoded string.
178pub fn verifying_key_from_hex(hex: &str) -> Result<VerifyingKey, SigningError> {
179    let bytes = hex_decode(hex)?;
180    let arr: [u8; 32] = bytes
181        .try_into()
182        .map_err(|_| SigningError::InvalidKey("public key must be 32 bytes".into()))?;
183    VerifyingKey::from_bytes(&arr)
184        .map_err(|e| SigningError::InvalidKey(format!("invalid public key: {}", e)))
185}
186
187// ---------------------------------------------------------------------------
188// Hex helpers (no external dependency needed)
189// ---------------------------------------------------------------------------
190
191fn hex_encode(bytes: &[u8]) -> String {
192    bytes.iter().map(|b| format!("{:02x}", b)).collect()
193}
194
195fn hex_decode(hex: &str) -> Result<Vec<u8>, SigningError> {
196    if !hex.len().is_multiple_of(2) {
197        return Err(SigningError::HexDecode(
198            "odd number of hex characters".into(),
199        ));
200    }
201    (0..hex.len())
202        .step_by(2)
203        .map(|i| {
204            u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| {
205                SigningError::HexDecode(format!("invalid hex at position {}: {}", i, e))
206            })
207        })
208        .collect()
209}
210
211// ---------------------------------------------------------------------------
212// Tests
213// ---------------------------------------------------------------------------
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_generate_keypair() {
221        let (kp, vk) = generate_keypair();
222        assert_eq!(kp.verifying_key(), vk);
223    }
224
225    #[test]
226    fn test_sign_and_verify() {
227        let (kp, vk) = generate_keypair();
228        let manifest = b"hello manifest";
229        let sig = sign_manifest(&kp, manifest);
230        let valid = verify_manifest(&vk, manifest, &sig).unwrap();
231        assert!(valid);
232    }
233
234    #[test]
235    fn test_tamper_detection() {
236        let (kp, vk) = generate_keypair();
237        let manifest = b"original manifest";
238        let sig = sign_manifest(&kp, manifest);
239        let valid = verify_manifest(&vk, b"tampered manifest", &sig).unwrap();
240        assert!(!valid, "tampered manifest should fail verification");
241    }
242
243    #[test]
244    fn test_wrong_key_rejection() {
245        let (kp1, _vk1) = generate_keypair();
246        let (_kp2, vk2) = generate_keypair();
247        let manifest = b"manifest for keypair 1";
248        let sig = sign_manifest(&kp1, manifest);
249        let valid = verify_manifest(&vk2, manifest, &sig).unwrap();
250        assert!(!valid, "wrong verifying key should fail");
251    }
252
253    #[test]
254    fn test_signed_manifest_roundtrip() {
255        let (kp, _vk) = generate_keypair();
256        let manifest = b"roundtrip manifest".to_vec();
257        let signed = sign_and_wrap(&kp, manifest.clone());
258        assert_eq!(signed.manifest, manifest);
259        let valid = verify_signed_manifest(&signed).unwrap();
260        assert!(valid);
261    }
262
263    #[test]
264    fn test_signed_manifest_tampered() {
265        let (kp, _vk) = generate_keypair();
266        let manifest = b"original data".to_vec();
267        let mut signed = sign_and_wrap(&kp, manifest);
268        signed.manifest = b"tampered data".to_vec();
269        let valid = verify_signed_manifest(&signed).unwrap();
270        assert!(!valid);
271    }
272
273    #[test]
274    fn test_key_hex_roundtrip() {
275        let (kp, _vk) = generate_keypair();
276        let secret_hex = kp.secret_key_hex();
277        let restored = SigningKeyPair::from_hex(&secret_hex).unwrap();
278        assert_eq!(restored.verifying_key(), kp.verifying_key());
279    }
280
281    #[test]
282    fn test_verifying_key_hex_roundtrip() {
283        let (_kp, vk) = generate_keypair();
284        let hex = hex_encode(vk.as_bytes());
285        let restored = verifying_key_from_hex(&hex).unwrap();
286        assert_eq!(restored, vk);
287    }
288
289    #[test]
290    fn test_invalid_hex_signature() {
291        let (_kp, vk) = generate_keypair();
292        let result = verify_manifest(&vk, b"data", "not_valid_hex!");
293        assert!(result.is_err());
294    }
295
296    #[test]
297    fn test_invalid_hex_key() {
298        let result = SigningKeyPair::from_hex("zzzz");
299        assert!(result.is_err());
300    }
301
302    #[test]
303    fn test_empty_manifest_signing() {
304        let (kp, vk) = generate_keypair();
305        let sig = sign_manifest(&kp, b"");
306        let valid = verify_manifest(&vk, b"", &sig).unwrap();
307        assert!(valid, "empty manifest should sign and verify");
308    }
309
310    #[test]
311    fn test_large_manifest_signing() {
312        let (kp, vk) = generate_keypair();
313        let manifest = vec![0xABu8; 1_000_000];
314        let sig = sign_manifest(&kp, &manifest);
315        let valid = verify_manifest(&vk, &manifest, &sig).unwrap();
316        assert!(valid, "large manifest should sign and verify");
317    }
318
319    #[test]
320    fn test_signature_is_hex_encoded() {
321        let (kp, _vk) = generate_keypair();
322        let sig = sign_manifest(&kp, b"data");
323        // 64 bytes = 128 hex characters
324        assert_eq!(sig.len(), 128);
325        assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
326    }
327
328    #[test]
329    fn test_signed_manifest_serialization() {
330        let (kp, _vk) = generate_keypair();
331        let signed = sign_and_wrap(&kp, b"json test".to_vec());
332        let json = serde_json::to_string(&signed).unwrap();
333        let deserialized: SignedManifest = serde_json::from_str(&json).unwrap();
334        let valid = verify_signed_manifest(&deserialized).unwrap();
335        assert!(valid, "deserialized signed manifest should verify");
336    }
337}