Skip to main content

astrid_crypto/
signature.rs

1//! Ed25519 signatures.
2//!
3//! Provides signature types and verification for:
4//! - Capability token signing
5//! - Audit entry signing
6//! - User identity verification
7
8use ed25519_dalek::{Signature as DalekSignature, Verifier, VerifyingKey};
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12use crate::error::{CryptoError, CryptoResult};
13
14/// An Ed25519 signature (64 bytes).
15#[derive(Clone, Copy, PartialEq, Eq)]
16pub struct Signature([u8; 64]);
17
18impl Signature {
19    /// Create from raw bytes.
20    #[must_use]
21    pub const fn from_bytes(bytes: [u8; 64]) -> Self {
22        Self(bytes)
23    }
24
25    /// Try to create from a slice.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`CryptoError::InvalidSignatureLength`] if the slice is not exactly 64 bytes.
30    pub fn try_from_slice(slice: &[u8]) -> CryptoResult<Self> {
31        if slice.len() != 64 {
32            return Err(CryptoError::InvalidSignatureLength {
33                expected: 64,
34                actual: slice.len(),
35            });
36        }
37        let mut bytes = [0u8; 64];
38        bytes.copy_from_slice(slice);
39        Ok(Self(bytes))
40    }
41
42    /// Get the raw bytes.
43    #[must_use]
44    pub const fn as_bytes(&self) -> &[u8; 64] {
45        &self.0
46    }
47
48    /// Encode as hex string.
49    #[must_use]
50    pub fn to_hex(&self) -> String {
51        hex::encode(self.0)
52    }
53
54    /// Decode from hex string.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if the string is not valid hex or not 64 bytes.
59    pub fn from_hex(s: &str) -> CryptoResult<Self> {
60        let bytes = hex::decode(s).map_err(|_| CryptoError::InvalidHexEncoding)?;
61        Self::try_from_slice(&bytes)
62    }
63
64    /// Encode as base64 string.
65    #[must_use]
66    pub fn to_base64(&self) -> String {
67        use base64::Engine;
68        base64::engine::general_purpose::STANDARD.encode(self.0)
69    }
70
71    /// Decode from base64 string.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the string is not valid base64 or not 64 bytes.
76    pub fn from_base64(s: &str) -> CryptoResult<Self> {
77        use base64::Engine;
78        let bytes = base64::engine::general_purpose::STANDARD
79            .decode(s)
80            .map_err(|_| CryptoError::InvalidBase64Encoding)?;
81        Self::try_from_slice(&bytes)
82    }
83
84    /// Verify this signature against a message and public key.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the public key is invalid or signature verification fails.
89    pub fn verify(&self, message: &[u8], public_key: &[u8; 32]) -> CryptoResult<()> {
90        let verifying_key = VerifyingKey::from_bytes(public_key)
91            .map_err(|e| CryptoError::InvalidPublicKey(e.to_string()))?;
92
93        let sig = DalekSignature::from_bytes(&self.0);
94
95        verifying_key
96            .verify(message, &sig)
97            .map_err(|_| CryptoError::SignatureVerificationFailed)
98    }
99
100    /// Convert to the underlying dalek signature type.
101    #[must_use]
102    pub fn to_dalek(&self) -> DalekSignature {
103        DalekSignature::from_bytes(&self.0)
104    }
105}
106
107impl fmt::Debug for Signature {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "Signature({}...)", &self.to_hex()[..16])
110    }
111}
112
113impl fmt::Display for Signature {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(f, "{}", self.to_hex())
116    }
117}
118
119impl Serialize for Signature {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: serde::Serializer,
123    {
124        serializer.serialize_str(&self.to_base64())
125    }
126}
127
128impl<'de> Deserialize<'de> for Signature {
129    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130    where
131        D: serde::Deserializer<'de>,
132    {
133        let s = String::deserialize(deserializer)?;
134        Self::from_base64(&s).map_err(serde::de::Error::custom)
135    }
136}
137
138impl From<DalekSignature> for Signature {
139    fn from(sig: DalekSignature) -> Self {
140        Self(sig.to_bytes())
141    }
142}
143
144impl From<Signature> for DalekSignature {
145    fn from(sig: Signature) -> Self {
146        DalekSignature::from_bytes(&sig.0)
147    }
148}
149
150impl From<[u8; 64]> for Signature {
151    fn from(bytes: [u8; 64]) -> Self {
152        Self(bytes)
153    }
154}
155
156impl From<Signature> for [u8; 64] {
157    fn from(sig: Signature) -> Self {
158        sig.0
159    }
160}
161
162impl AsRef<[u8]> for Signature {
163    fn as_ref(&self) -> &[u8] {
164        &self.0
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::KeyPair;
172
173    #[test]
174    fn test_signature_encoding() {
175        let keypair = KeyPair::generate();
176        let message = b"test message";
177        let sig = keypair.sign(message);
178
179        // Hex roundtrip
180        let hex = sig.to_hex();
181        let decoded = Signature::from_hex(&hex).unwrap();
182        assert_eq!(sig, decoded);
183
184        // Base64 roundtrip
185        let b64 = sig.to_base64();
186        let decoded = Signature::from_base64(&b64).unwrap();
187        assert_eq!(sig, decoded);
188    }
189
190    #[test]
191    fn test_signature_verification() {
192        let keypair = KeyPair::generate();
193        let message = b"test message";
194        let sig = keypair.sign(message);
195
196        // Should verify with correct public key
197        assert!(sig.verify(message, keypair.public_key_bytes()).is_ok());
198
199        // Should fail with wrong message
200        assert!(
201            sig.verify(b"wrong message", keypair.public_key_bytes())
202                .is_err()
203        );
204
205        // Should fail with wrong public key
206        let other_keypair = KeyPair::generate();
207        assert!(
208            sig.verify(message, other_keypair.public_key_bytes())
209                .is_err()
210        );
211    }
212
213    #[test]
214    fn test_invalid_signature_length() {
215        let result = Signature::try_from_slice(&[0u8; 63]);
216        assert!(matches!(
217            result,
218            Err(CryptoError::InvalidSignatureLength { .. })
219        ));
220    }
221}