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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Purpose {
17 DeviceCert,
19 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
32fn 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
41pub struct DeviceKey(SigningKey);
44
45impl DeviceKey {
46 pub fn generate() -> Self {
48 Self::from_seed(rand::random())
49 }
50
51 pub fn from_seed(seed: [u8; 32]) -> Self {
53 Self(SigningKey::from_bytes(&seed))
54 }
55
56 pub fn seed(&self) -> [u8; 32] {
58 self.0.to_bytes()
59 }
60
61 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 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 pub fn keypair_bytes(&self) -> [u8; 64] {
91 self.0.to_keypair_bytes()
92 }
93
94 pub fn public(&self) -> PublicKey {
96 PublicKey(self.0.verifying_key().to_bytes())
97 }
98
99 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#[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 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 pub fn as_bytes(&self) -> &[u8; 32] {
125 &self.0
126 }
127
128 pub fn fingerprint(&self) -> String {
130 self.to_string()[..8].to_string()
131 }
132
133 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#[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#[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 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}