Skip to main content

substrate/crypto/
signature.rs

1use anyhow::{anyhow, Result};
2use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
3use serde::Serialize;
4
5use super::AlgorithmBytes;
6
7const SIGNATURE_SEPARATOR: char = '.';
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SignatureAlgorithm {
11    Ed25519,
12}
13
14pub fn parse_signature(value: &str) -> Result<AlgorithmBytes<SignatureAlgorithm>> {
15    let (algorithm_str, value_b58) = value
16        .split_once(SIGNATURE_SEPARATOR)
17        .ok_or_else(|| anyhow!("Signature must use '{{algorithm}}.{{base58}}' format"))?;
18    let algorithm = match algorithm_str {
19        "ed25519" => SignatureAlgorithm::Ed25519,
20        other => return Err(anyhow!("Unsupported signature algorithm: '{}'", other)),
21    };
22    if value_b58.is_empty() {
23        return Err(anyhow!("Signature payload must not be empty"));
24    }
25
26    let bytes = bs58::decode(value_b58)
27        .into_vec()
28        .map_err(|e| anyhow!("Invalid signature base58 payload: {}", e))?;
29    if bytes.len() != 64 {
30        return Err(anyhow!(
31            "Invalid signature length for {}: expected 64 bytes, got {}",
32            algorithm.prefix(),
33            bytes.len()
34        ));
35    }
36
37    Ok(AlgorithmBytes { algorithm, bytes })
38}
39
40pub fn format_signature(algorithm: SignatureAlgorithm, bytes: &[u8]) -> String {
41    format!(
42        "{}.{}",
43        algorithm.prefix(),
44        bs58::encode(bytes).into_string()
45    )
46}
47
48pub fn compute_signature<T: Serialize>(
49    value: &T,
50    algorithm: SignatureAlgorithm,
51    private_key_bytes: &[u8],
52) -> Result<String> {
53    let canonical = canonicalize_json(value)?;
54
55    match algorithm {
56        SignatureAlgorithm::Ed25519 => {
57            let private_key_bytes: [u8; 32] = private_key_bytes.try_into().map_err(|_| {
58                anyhow!("Invalid private key length for ed25519: expected 32 bytes")
59            })?;
60            let signing_key = SigningKey::from_bytes(&private_key_bytes);
61            let signature = signing_key.sign(canonical.as_bytes());
62            Ok(format_signature(
63                SignatureAlgorithm::Ed25519,
64                &signature.to_bytes(),
65            ))
66        }
67    }
68}
69
70/// Verify a raw signature against raw data.
71pub fn verify_signature(data: &[u8], signature_str: &str, public_key: &str) -> Result<()> {
72    let signature = parse_signature(signature_str)?;
73    let public_key = super::parse_key(public_key)?;
74
75    match (signature.algorithm, public_key.algorithm) {
76        (SignatureAlgorithm::Ed25519, super::KeyAlgorithm::Ed25519) => {
77            verify_ed25519(data, &signature.bytes, &public_key.bytes)
78        }
79    }
80}
81
82pub fn verify_json_signature<T: Serialize>(
83    value: &T,
84    signature: &str,
85    public_key: &str,
86) -> Result<()> {
87    let canonical = canonicalize_json(value)?;
88    verify_signature(canonical.as_bytes(), signature, public_key)
89}
90
91impl SignatureAlgorithm {
92    fn prefix(self) -> &'static str {
93        match self {
94            Self::Ed25519 => "ed25519",
95        }
96    }
97}
98
99fn canonicalize_json<T: Serialize>(value: &T) -> Result<String> {
100    serde_jcs::to_string(value).map_err(|e| anyhow!("JCS serialization failed: {}", e))
101}
102
103fn verify_ed25519(data: &[u8], signature_bytes: &[u8], public_key_bytes: &[u8]) -> Result<()> {
104    let public_key_bytes: [u8; 32] = public_key_bytes
105        .try_into()
106        .map_err(|_| anyhow!("Invalid public key length (expected 32 bytes for ed25519)"))?;
107    let verifying_key = VerifyingKey::from_bytes(&public_key_bytes)?;
108
109    let signature_bytes: [u8; 64] = signature_bytes
110        .try_into()
111        .map_err(|_| anyhow!("Invalid signature length (expected 64 bytes for ed25519)"))?;
112    let signature = Signature::from_bytes(&signature_bytes);
113
114    verifying_key
115        .verify(data, &signature)
116        .map_err(|e| anyhow!("Signature verification failed: {}", e))?;
117
118    Ok(())
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
123mod tests {
124
125    use super::*;
126    use crate::{format_key, KeyAlgorithm};
127    use serde_json::json;
128
129    #[test]
130    fn test_parse_signature_roundtrip() {
131        let bytes = vec![7u8; 64];
132        let encoded = format_signature(SignatureAlgorithm::Ed25519, &bytes);
133        let parsed = parse_signature(&encoded).unwrap();
134        assert_eq!(parsed.algorithm, SignatureAlgorithm::Ed25519);
135        assert_eq!(parsed.bytes, bytes);
136    }
137
138    #[test]
139    fn test_parse_signature_rejects_wrong_length() {
140        let encoded = format_signature(SignatureAlgorithm::Ed25519, &[1, 2, 3]);
141        assert!(parse_signature(&encoded).is_err());
142    }
143
144    #[test]
145    fn test_compute_and_verify_json_signature_roundtrip() {
146        let private_key = [3u8; 32];
147        let signing_key = SigningKey::from_bytes(&private_key);
148        let public_key = format_key(
149            KeyAlgorithm::Ed25519,
150            &signing_key.verifying_key().to_bytes(),
151        );
152        let value = json!({"b": 2, "a": 1});
153
154        let signature =
155            compute_signature(&value, SignatureAlgorithm::Ed25519, &private_key).unwrap();
156        verify_json_signature(&value, &signature, &public_key).unwrap();
157    }
158
159    #[test]
160    fn test_verify_signature_missing_prefix() {
161        let result = verify_signature(b"test", "aabbcc", "ed25519.aabbccdd");
162        assert!(result.is_err());
163    }
164
165    #[test]
166    fn test_verify_signature_key_missing_prefix() {
167        let result = verify_signature(b"test", "ed25519.aabbcc", "aabbccdd");
168        assert!(result.is_err());
169    }
170
171    #[test]
172    fn test_verify_signature_wrong_length() {
173        let result = verify_signature(b"test", "ed25519.5Hue", "ed25519.5Hue");
174        assert!(result.is_err());
175    }
176
177    #[test]
178    fn test_sign_and_verify_roundtrip() {
179        let secret_bytes: [u8; 32] = [
180            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
181            25, 26, 27, 28, 29, 30, 31, 32,
182        ];
183        let signing_key = SigningKey::from_bytes(&secret_bytes);
184        let public_key = format_key(
185            super::super::KeyAlgorithm::Ed25519,
186            &signing_key.verifying_key().to_bytes(),
187        );
188        let data = json!({
189            "author": "test.com",
190            "intent": ["v1.0"],
191            "license": "MIT",
192            "name": "test-spore",
193            "synopsis": "A test spore"
194        });
195
196        let signature =
197            compute_signature(&data, super::SignatureAlgorithm::Ed25519, &secret_bytes).unwrap();
198        let canonical = serde_jcs::to_string(&data).unwrap();
199
200        assert!(verify_signature(canonical.as_bytes(), &signature, &public_key).is_ok());
201        assert!(verify_signature(b"wrong data", &signature, &public_key).is_err());
202    }
203}