Skip to main content

edda_auth/
keys.rs

1use std::{
2    fmt, fs,
3    io::{self, Write},
4    path::Path,
5    str::FromStr,
6};
7
8use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
9use serde::{Deserialize, Serialize};
10
11use crate::{AuthError, bytes};
12
13/// What a signature is for. Part of every signed message, so a signature
14/// can't be reused for a different purpose than the one it was made for.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Purpose {
17    /// A [`crate::DeviceCert`], signed by the DM device vouching for it.
18    DeviceCert,
19    /// A [`crate::protocol::LoginProof`], answering a login challenge.
20    Login,
21}
22
23impl Purpose {
24    fn label(self) -> &'static str {
25        match self {
26            Purpose::DeviceCert => "edda/v1/device-cert",
27            Purpose::Login => "edda/v1/login",
28        }
29    }
30}
31
32/// The exact bytes signed for `value`: the purpose's label, a NUL (labels
33/// never contain one, so the split is unambiguous), then the value's
34/// canonical encoding.
35fn message(purpose: Purpose, value: &impl Serialize) -> Result<Vec<u8>, AuthError> {
36    let mut message = purpose.label().as_bytes().to_vec();
37    message.push(0);
38    postcard::to_extend(value, message).map_err(|e| AuthError::Encoding(e.to_string()))
39}
40
41/// A device's private signing key. Generated on the device and never sent
42/// anywhere.
43pub struct DeviceKey(SigningKey);
44
45impl DeviceKey {
46    /// A new random key.
47    pub fn generate() -> Self {
48        Self::from_seed(rand::random())
49    }
50
51    /// The key made from `seed`, as stored in `device.key`.
52    pub fn from_seed(seed: [u8; 32]) -> Self {
53        Self(SigningKey::from_bytes(&seed))
54    }
55
56    /// The 32 secret bytes everything else derives from; what to store.
57    pub fn seed(&self) -> [u8; 32] {
58        self.0.to_bytes()
59    }
60
61    /// Reads a key saved with [`DeviceKey::save`]; `None` if there's no
62    /// such file.
63    pub fn load(path: &Path) -> io::Result<Option<Self>> {
64        match fs::read_to_string(path) {
65            Ok(text) => {
66                let seed = bytes::decode(&text)
67                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
68                Ok(Some(Self::from_seed(seed)))
69            }
70            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
71            Err(e) => Err(e),
72        }
73    }
74
75    /// Writes the seed to `path`, readable by the owner only. Refuses to
76    /// overwrite an existing key.
77    pub fn save(&self, path: &Path) -> io::Result<()> {
78        if let Some(dir) = path.parent() {
79            fs::create_dir_all(dir)?;
80        }
81        let mut options = fs::OpenOptions::new();
82        options.write(true).create_new(true);
83        #[cfg(unix)]
84        std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
85        let mut file = options.open(path)?;
86        writeln!(file, "{}", bytes::encode(&self.seed()))
87    }
88
89    /// Seed and public key, as PASETO v4 secret keys are laid out.
90    pub fn keypair_bytes(&self) -> [u8; 64] {
91        self.0.to_keypair_bytes()
92    }
93
94    /// The key others check this device's signatures with.
95    pub fn public(&self) -> PublicKey {
96        PublicKey(self.0.verifying_key().to_bytes())
97    }
98
99    /// Signs `value` for `purpose` (see the crate's docs for what is signed).
100    pub fn sign(&self, purpose: Purpose, value: &impl Serialize) -> Result<Signature, AuthError> {
101        Ok(Signature(self.0.sign(&message(purpose, value)?).to_bytes()))
102    }
103}
104
105impl fmt::Debug for DeviceKey {
106    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107        write!(f, "DeviceKey({})", self.public())
108    }
109}
110
111/// An Ed25519 public key. Shown as unpadded base64url.
112#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
113#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(value_type = String))]
114pub struct PublicKey(#[serde(with = "bytes")] [u8; 32]);
115
116impl PublicKey {
117    /// Fails for byte strings that aren't a point on the curve.
118    pub fn from_bytes(bytes: [u8; 32]) -> Result<Self, AuthError> {
119        VerifyingKey::from_bytes(&bytes).map_err(|_| AuthError::InvalidKey)?;
120        Ok(Self(bytes))
121    }
122
123    /// The key's 32 bytes.
124    pub fn as_bytes(&self) -> &[u8; 32] {
125        &self.0
126    }
127
128    /// A short form for people to compare and refer to keys by.
129    pub fn fingerprint(&self) -> String {
130        self.to_string()[..8].to_string()
131    }
132
133    /// Checks `signature` over `value` for `purpose`. Strict verification:
134    /// rejects weak keys and non-canonical signatures.
135    pub fn verify(
136        &self,
137        purpose: Purpose,
138        value: &impl Serialize,
139        signature: &Signature,
140    ) -> Result<(), AuthError> {
141        let key = VerifyingKey::from_bytes(&self.0).map_err(|_| AuthError::InvalidKey)?;
142        let signature = ed25519_dalek::Signature::from_bytes(&signature.0);
143        key.verify_strict(&message(purpose, value)?, &signature)
144            .map_err(|_| AuthError::BadSignature)
145    }
146}
147
148impl fmt::Display for PublicKey {
149    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150        f.write_str(&bytes::encode(&self.0))
151    }
152}
153
154impl fmt::Debug for PublicKey {
155    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156        write!(f, "PublicKey({self})")
157    }
158}
159
160impl FromStr for PublicKey {
161    type Err = AuthError;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        Self::from_bytes(bytes::decode(s).map_err(AuthError::Encoding)?)
165    }
166}
167
168/// An Ed25519 signature. Shown as unpadded base64url.
169#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(value_type = String))]
171pub struct Signature(#[serde(with = "bytes")] [u8; 64]);
172
173impl fmt::Debug for Signature {
174    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
175        write!(f, "Signature({})", bytes::encode(&self.0))
176    }
177}
178
179/// 32 random bytes, for challenges. Shown as unpadded base64url.
180#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(value_type = String))]
182pub struct Nonce(#[serde(with = "bytes")] pub [u8; 32]);
183
184impl Nonce {
185    /// A fresh random nonce.
186    pub fn random() -> Self {
187        Self(rand::random())
188    }
189}
190
191impl fmt::Debug for Nonce {
192    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
193        write!(f, "Nonce({})", bytes::encode(&self.0))
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn signatures_are_bound_to_key_value_and_purpose() {
203        let key = DeviceKey::generate();
204        let signature = key.sign(Purpose::Login, &"hello").unwrap();
205        let public = key.public();
206        assert_eq!(public.verify(Purpose::Login, &"hello", &signature), Ok(()));
207        assert_eq!(
208            public.verify(Purpose::Login, &"hellO", &signature),
209            Err(AuthError::BadSignature)
210        );
211        assert_eq!(
212            public.verify(Purpose::DeviceCert, &"hello", &signature),
213            Err(AuthError::BadSignature),
214            "a login signature is no use as a certificate signature"
215        );
216        let other = DeviceKey::generate().public();
217        assert_eq!(
218            other.verify(Purpose::Login, &"hello", &signature),
219            Err(AuthError::BadSignature)
220        );
221    }
222
223    #[test]
224    fn keys_round_trip_through_text_and_seed() {
225        let key = DeviceKey::generate();
226        let public = key.public();
227        assert_eq!(public.to_string().len(), 43);
228        assert_eq!(public.to_string().parse::<PublicKey>(), Ok(public));
229        assert_eq!(DeviceKey::from_seed(key.seed()).public(), public);
230        let json = serde_json::to_string(&public).unwrap();
231        assert_eq!(json, format!("\"{public}\""));
232        assert_eq!(serde_json::from_str::<PublicKey>(&json).unwrap(), public);
233        assert!("not a key".parse::<PublicKey>().is_err());
234    }
235}