Skip to main content

starweft_crypto/
lib.rs

1//! Cryptographic primitives for the Starweft protocol.
2//!
3//! Provides Ed25519 key generation, message signing, signature verification,
4//! and deterministic canonical JSON serialization for envelope integrity.
5
6use base64::Engine;
7use base64::engine::general_purpose::STANDARD;
8use ed25519_dalek::{Signature as DalekSignature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand_core::OsRng;
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12use starweft_id::KeyId;
13use thiserror::Error;
14use time::OffsetDateTime;
15
16/// Errors that can occur during cryptographic operations.
17#[derive(Debug, Error)]
18pub enum CryptoError {
19    /// The secret key bytes could not be decoded or are invalid.
20    #[error("invalid secret key bytes")]
21    InvalidSecretKey,
22    /// The public key bytes could not be decoded or are invalid.
23    #[error("invalid public key bytes")]
24    InvalidPublicKey,
25    /// The signature bytes could not be decoded or are malformed.
26    #[error("invalid signature bytes")]
27    InvalidSignature,
28    /// The signature did not match the payload and public key.
29    #[error("signature verification failed")]
30    SignatureVerificationFailed,
31    /// JSON serialization or deserialization failed.
32    #[error("serialization failed: {0}")]
33    Serialization(#[from] serde_json::Error),
34    /// An I/O operation failed (e.g. reading/writing key files).
35    #[error("io failed: {0}")]
36    Io(#[from] std::io::Error),
37}
38
39/// A detached Ed25519 signature with algorithm and key metadata.
40#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct MessageSignature {
42    /// Signature algorithm identifier (always `"ed25519"`).
43    pub alg: String,
44    /// Identifier of the key that produced this signature.
45    pub key_id: KeyId,
46    /// Base64-encoded signature bytes.
47    pub sig: String,
48}
49
50/// An Ed25519 keypair stored as base64-encoded strings with metadata.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct StoredKeypair {
53    /// Unique identifier for this keypair.
54    pub key_id: KeyId,
55    /// Timestamp when the keypair was generated.
56    pub created_at: OffsetDateTime,
57    /// Base64-encoded Ed25519 secret key (32 bytes).
58    pub secret_key: String,
59    /// Base64-encoded Ed25519 public key (32 bytes).
60    pub public_key: String,
61}
62
63impl StoredKeypair {
64    /// Generates a new random Ed25519 keypair.
65    #[must_use]
66    pub fn generate() -> Self {
67        let signing_key = SigningKey::generate(&mut OsRng);
68        let verifying_key = signing_key.verifying_key();
69
70        Self {
71            key_id: KeyId::generate(),
72            created_at: OffsetDateTime::now_utc(),
73            secret_key: STANDARD.encode(signing_key.to_bytes()),
74            public_key: STANDARD.encode(verifying_key.to_bytes()),
75        }
76    }
77
78    /// Decodes and returns the Ed25519 signing key.
79    pub fn signing_key(&self) -> Result<SigningKey, CryptoError> {
80        let bytes = decode_32_bytes(&self.secret_key).ok_or(CryptoError::InvalidSecretKey)?;
81        Ok(SigningKey::from_bytes(&bytes))
82    }
83
84    /// Decodes and returns the Ed25519 verifying (public) key.
85    pub fn verifying_key(&self) -> Result<VerifyingKey, CryptoError> {
86        let bytes = decode_32_bytes(&self.public_key).ok_or(CryptoError::InvalidPublicKey)?;
87        VerifyingKey::from_bytes(&bytes).map_err(|_| CryptoError::InvalidPublicKey)
88    }
89
90    /// Returns the raw 32-byte secret key.
91    pub fn secret_key_bytes(&self) -> Result<[u8; 32], CryptoError> {
92        decode_32_bytes(&self.secret_key).ok_or(CryptoError::InvalidSecretKey)
93    }
94
95    /// Signs raw bytes and returns a detached [`MessageSignature`].
96    pub fn sign_bytes(&self, payload: &[u8]) -> Result<MessageSignature, CryptoError> {
97        let signing_key = self.signing_key()?;
98        let signature = signing_key.sign(payload);
99        Ok(MessageSignature {
100            alg: "ed25519".to_owned(),
101            key_id: self.key_id.clone(),
102            sig: STANDARD.encode(signature.to_bytes()),
103        })
104    }
105
106    /// Serializes `payload` to canonical JSON, then signs the bytes.
107    pub fn sign_json<T: Serialize>(&self, payload: &T) -> Result<MessageSignature, CryptoError> {
108        self.sign_bytes(&canonical_json(payload)?)
109    }
110
111    /// Writes the keypair to a JSON file with restrictive permissions (0600 on Unix).
112    pub fn write_to_path(&self, path: &std::path::Path) -> Result<(), CryptoError> {
113        if let Some(parent) = path.parent() {
114            std::fs::create_dir_all(parent)?;
115        }
116        // Clear read-only before overwrite (Windows sets read-only for protection).
117        // On Unix, set_private_permissions uses mode 0o600, not readonly, so this
118        // branch only activates on Windows where readonly was previously set.
119        #[allow(clippy::permissions_set_readonly_false)]
120        if path.exists() {
121            let mut perms = std::fs::metadata(path)?.permissions();
122            if perms.readonly() {
123                perms.set_readonly(false);
124                std::fs::set_permissions(path, perms)?;
125            }
126        }
127        std::fs::write(path, serde_json::to_vec_pretty(self)?)?;
128        set_private_permissions(path)?;
129        Ok(())
130    }
131
132    /// Reads a keypair from a JSON file at the given path.
133    pub fn read_from_path(path: &std::path::Path) -> Result<Self, CryptoError> {
134        let bytes = std::fs::read(path)?;
135        Ok(serde_json::from_slice(&bytes)?)
136    }
137}
138
139/// Serializes a value to deterministic canonical JSON bytes.
140///
141/// Keys are sorted recursively at every level to produce identical byte
142/// output regardless of field insertion order or `serde_json` feature flags.
143pub fn canonical_json<T: Serialize>(payload: &T) -> Result<Vec<u8>, CryptoError> {
144    let value = sort_json_keys_recursive(serde_json::to_value(payload)?);
145    Ok(serde_json::to_vec(&value)?)
146}
147
148/// Recursively sorts object keys. `serde_json::Map` is backed by `BTreeMap`
149/// by default (keys already sorted), but we recurse into nested values to
150/// guarantee canonical output even if `preserve_order` is ever enabled.
151fn sort_json_keys_recursive(value: serde_json::Value) -> serde_json::Value {
152    match value {
153        serde_json::Value::Object(map) => {
154            let sorted: serde_json::Map<String, serde_json::Value> = map
155                .into_iter()
156                .map(|(k, v)| (k, sort_json_keys_recursive(v)))
157                .collect();
158            serde_json::Value::Object(sorted)
159        }
160        serde_json::Value::Array(arr) => {
161            serde_json::Value::Array(arr.into_iter().map(sort_json_keys_recursive).collect())
162        }
163        other => other,
164    }
165}
166
167/// Decodes a base64-encoded Ed25519 public key into a [`VerifyingKey`].
168pub fn verifying_key_from_base64(encoded: &str) -> Result<VerifyingKey, CryptoError> {
169    let bytes = decode_32_bytes(encoded).ok_or(CryptoError::InvalidPublicKey)?;
170    VerifyingKey::from_bytes(&bytes).map_err(|_| CryptoError::InvalidPublicKey)
171}
172
173/// Verifies a signature against canonical JSON of the payload.
174pub fn verify_json<T: Serialize>(
175    verifying_key: &VerifyingKey,
176    payload: &T,
177    signature: &MessageSignature,
178) -> Result<(), CryptoError> {
179    verify_bytes(verifying_key, &canonical_json(payload)?, signature)
180}
181
182/// Verifies a signature against raw payload bytes.
183pub fn verify_bytes(
184    verifying_key: &VerifyingKey,
185    payload: &[u8],
186    signature: &MessageSignature,
187) -> Result<(), CryptoError> {
188    let signature_bytes = STANDARD
189        .decode(signature.sig.as_bytes())
190        .map_err(|_| CryptoError::InvalidSignature)?;
191    let signature = DalekSignature::try_from(signature_bytes.as_slice())
192        .map_err(|_| CryptoError::InvalidSignature)?;
193
194    verifying_key
195        .verify(payload, &signature)
196        .map_err(|_| CryptoError::SignatureVerificationFailed)
197}
198
199/// Reads and deserializes a JSON file from the given path.
200pub fn read_json_file<T: DeserializeOwned>(path: &std::path::Path) -> Result<T, CryptoError> {
201    let bytes = std::fs::read(path)?;
202    Ok(serde_json::from_slice(&bytes)?)
203}
204
205fn decode_32_bytes(encoded: &str) -> Option<[u8; 32]> {
206    let bytes = STANDARD.decode(encoded.as_bytes()).ok()?;
207    bytes.try_into().ok()
208}
209
210#[cfg(unix)]
211fn set_private_permissions(path: &std::path::Path) -> Result<(), std::io::Error> {
212    use std::os::unix::fs::PermissionsExt;
213
214    let permissions = std::fs::Permissions::from_mode(0o600);
215    std::fs::set_permissions(path, permissions)
216}
217
218#[cfg(not(unix))]
219fn set_private_permissions(path: &std::path::Path) -> Result<(), std::io::Error> {
220    // Mark private key files as read-only on Windows to prevent accidental
221    // modification. Directory-level ACL protection is applied by the
222    // application's config layer (see config.rs ensure_layout).
223    let mut perms = std::fs::metadata(path)?.permissions();
224    perms.set_readonly(true);
225    std::fs::set_permissions(path, perms)
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn can_sign_and_verify_json() {
234        let keypair = StoredKeypair::generate();
235        let signature = keypair
236            .sign_json(&serde_json::json!({ "message": "hello" }))
237            .expect("signature");
238
239        let verifying_key = keypair.verifying_key().expect("verifying key");
240        verify_json(
241            &verifying_key,
242            &serde_json::json!({ "message": "hello" }),
243            &signature,
244        )
245        .expect("verify");
246    }
247
248    #[test]
249    fn canonical_json_sorts_nested_keys() {
250        // Build JSON with known key order via serde_json::json!
251        let input = serde_json::json!({
252            "z": 1,
253            "a": { "c": 3, "b": 2 },
254            "m": [{ "y": 4, "x": 5 }]
255        });
256        let bytes = canonical_json(&input).expect("canonical");
257        // Keys must be alphabetically sorted at every level
258        let expected = r#"{"a":{"b":2,"c":3},"m":[{"x":5,"y":4}],"z":1}"#;
259        assert_eq!(String::from_utf8(bytes).expect("utf8"), expected);
260    }
261}