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
70pub 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}