Skip to main content

shape_runtime/crypto/
signing.rs

1//! Ed25519 digital signature support for module manifests.
2//!
3//! Provides signing and verification of content-addressed module manifests
4//! using Ed25519 key pairs.
5
6use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
7use serde::{Deserialize, Serialize};
8
9/// Cryptographic signature data attached to a module manifest.
10///
11/// Contains the author's public key, the Ed25519 signature over the manifest
12/// hash, and a timestamp recording when the signature was produced.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ModuleSignatureData {
15    /// Ed25519 public key of the author (32 bytes).
16    pub author_key: [u8; 32],
17    /// Ed25519 signature bytes (64 bytes). Uses `Vec<u8>` because serde does
18    /// not support `[u8; 64]` out of the box.
19    pub signature: Vec<u8>,
20    /// Unix timestamp (seconds) when the signature was created.
21    pub signed_at: u64,
22}
23
24impl ModuleSignatureData {
25    /// Sign a manifest hash with the given signing key.
26    ///
27    /// Produces a `ModuleSignatureData` containing the author's public key,
28    /// the signature over `manifest_hash`, and the current timestamp.
29    pub fn sign(manifest_hash: &[u8; 32], signing_key: &SigningKey) -> Self {
30        let signature = signing_key.sign(manifest_hash);
31        let author_key = signing_key.verifying_key().to_bytes();
32        Self {
33            author_key,
34            signature: signature.to_bytes().to_vec(),
35            signed_at: std::time::SystemTime::now()
36                .duration_since(std::time::UNIX_EPOCH)
37                .unwrap_or_default()
38                .as_secs(),
39        }
40    }
41
42    /// Verify that this signature is valid for the given manifest hash.
43    ///
44    /// Returns `true` if the Ed25519 signature is valid for the embedded
45    /// author public key and the provided hash.
46    pub fn verify(&self, manifest_hash: &[u8; 32]) -> bool {
47        let Ok(verifying_key) = VerifyingKey::from_bytes(&self.author_key) else {
48            return false;
49        };
50        let Ok(sig_bytes): Result<[u8; 64], _> = self.signature.as_slice().try_into() else {
51            return false;
52        };
53        let signature = Signature::from_bytes(&sig_bytes);
54        verifying_key.verify(manifest_hash, &signature).is_ok()
55    }
56}
57
58/// Sign a manifest hash using raw secret key bytes (32 bytes).
59///
60/// This is a convenience wrapper that avoids callers needing to depend on
61/// `ed25519_dalek` directly.
62pub fn sign_manifest_hash(
63    manifest_hash: &[u8; 32],
64    secret_key_bytes: &[u8; 32],
65) -> ModuleSignatureData {
66    let signing_key = SigningKey::from_bytes(secret_key_bytes);
67    ModuleSignatureData::sign(manifest_hash, &signing_key)
68}
69
70/// Get the public key bytes for a given secret key.
71pub fn public_key_from_secret(secret_key_bytes: &[u8; 32]) -> [u8; 32] {
72    let signing_key = SigningKey::from_bytes(secret_key_bytes);
73    signing_key.verifying_key().to_bytes()
74}
75
76/// Generate a new Ed25519 signing/verifying key pair.
77pub fn generate_keypair() -> (SigningKey, VerifyingKey) {
78    let mut secret = [0u8; 32];
79    use rand::RngCore;
80    rand::thread_rng().fill_bytes(&mut secret);
81    let signing_key = SigningKey::from_bytes(&secret);
82    let verifying_key = signing_key.verifying_key();
83    (signing_key, verifying_key)
84}
85
86/// Generate a new Ed25519 key pair, returning raw byte arrays.
87///
88/// Returns `(secret_key_bytes, public_key_bytes)`. This avoids callers
89/// needing to depend on `ed25519_dalek` types directly.
90pub fn generate_keypair_bytes() -> ([u8; 32], [u8; 32]) {
91    let (signing, verifying) = generate_keypair();
92    (signing.to_bytes(), verifying.to_bytes())
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_sign_and_verify() {
101        let (signing_key, _) = generate_keypair();
102        let manifest_hash = [42u8; 32];
103        let sig = ModuleSignatureData::sign(&manifest_hash, &signing_key);
104        assert!(sig.verify(&manifest_hash));
105    }
106
107    #[test]
108    fn test_verify_fails_with_wrong_hash() {
109        let (signing_key, _) = generate_keypair();
110        let manifest_hash = [42u8; 32];
111        let sig = ModuleSignatureData::sign(&manifest_hash, &signing_key);
112        let wrong_hash = [99u8; 32];
113        assert!(!sig.verify(&wrong_hash));
114    }
115
116    #[test]
117    fn test_verify_fails_with_corrupt_signature() {
118        let (signing_key, _) = generate_keypair();
119        let manifest_hash = [42u8; 32];
120        let mut sig = ModuleSignatureData::sign(&manifest_hash, &signing_key);
121        sig.signature[0] ^= 0xFF;
122        assert!(!sig.verify(&manifest_hash));
123    }
124
125    #[test]
126    fn test_verify_fails_with_wrong_key() {
127        let (signing_key, _) = generate_keypair();
128        let (other_key, _) = generate_keypair();
129        let manifest_hash = [42u8; 32];
130        let mut sig = ModuleSignatureData::sign(&manifest_hash, &signing_key);
131        sig.author_key = other_key.verifying_key().to_bytes();
132        assert!(!sig.verify(&manifest_hash));
133    }
134
135    #[test]
136    fn test_signed_at_is_nonzero() {
137        let (signing_key, _) = generate_keypair();
138        let sig = ModuleSignatureData::sign(&[0u8; 32], &signing_key);
139        assert!(sig.signed_at > 0);
140    }
141
142    #[test]
143    fn test_serde_roundtrip() {
144        let (signing_key, _) = generate_keypair();
145        let manifest_hash = [7u8; 32];
146        let sig = ModuleSignatureData::sign(&manifest_hash, &signing_key);
147
148        let json = serde_json::to_string(&sig).expect("serialize");
149        let restored: ModuleSignatureData = serde_json::from_str(&json).expect("deserialize");
150
151        assert_eq!(restored.author_key, sig.author_key);
152        assert_eq!(restored.signature, sig.signature);
153        assert_eq!(restored.signed_at, sig.signed_at);
154        assert!(restored.verify(&manifest_hash));
155    }
156}