Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_sbj/src/key.rs

10.1 KiB, 5 runs

created by r1870400018:22218, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The signing key an author holds, and the file it is kept in.
2//!
3//! A document's signature binds its address to its author, so an authoring tool needs a key, and a
4//! key that changes between runs gives one document a new signature every time it is written. The
5//! key is therefore a file, in the same JDAT text form the fixtures' `key.jdat` uses: a map naming
6//! the signature scheme and carrying the public and secret keys as raw bytes.
7//!
8//! A key file may carry the pair directly, or hold several named pairs, as the fixture key does with
9//! its `author` and its `impostor`. [`load`] takes the name of the entry to read, and reads the pair
10//! at the top level when given none.
11
12use crate::text;
13
14use oxedyne_fe2o3_core::{
15 prelude::*,
16 file as core_file,
17};
18use oxedyne_fe2o3_crypto::sign::SignatureScheme;
19use oxedyne_fe2o3_iop_crypto::{
20 keys::KeyManager,
21 sign::Signer,
22};
23use oxedyne_fe2o3_jdat::prelude::*;
24
25use std::{
26 fs,
27 path::Path,
28};
29
30/// The name the key file gives the v0 signature scheme.
31pub const SCHEME_ED25519: &'static str = "ed25519";
32
33/// The key-file key naming the signature scheme.
34pub const KEY_SCHEME: &'static str = "scheme";
35/// The key-file key carrying the public key.
36pub const KEY_PK: &'static str = "pk";
37/// The key-file key carrying the secret key.
38pub const KEY_SK: &'static str = "sk";
39
40/// An Ed25519 key pair, held as raw bytes.
41#[derive(Clone, Debug)]
42pub struct KeyPair {
43 /// The public key, which an envelope names as its author.
44 pub pk: Vec<u8>,
45 /// The secret key, which signs.
46 pub sk: Vec<u8>,
47}
48
49impl KeyPair {
50
51 /// Generates a fresh Ed25519 key pair.
52 pub fn generate() -> Outcome<Self> {
53 let signer = SignatureScheme::new_ed25519();
54 let pk = match res!(signer.get_public_key()) {
55 Some(pk) => pk.to_vec(),
56 None => return Err(err!(
57 "A fresh Ed25519 signer holds no public key."; Bug, Missing)),
58 };
59 let sk = match res!(signer.get_secret_key()) {
60 Some(sk) => sk.to_vec(),
61 None => return Err(err!(
62 "A fresh Ed25519 signer holds no secret key."; Bug, Missing)),
63 };
64 Ok(Self {
65 pk,
66 sk,
67 })
68 }
69
70 /// A signer holding this pair, which is what `doc::write` signs an envelope with.
71 pub fn signer(&self) -> Outcome<SignatureScheme> {
72 Ok(res!(SignatureScheme::empty_ed25519().clone_with_keys(Some(&self.pk), Some(&self.sk))))
73 }
74
75 /// The pair as a daticle, the `pk` and `sk` of a key file.
76 pub fn to_dat(&self) -> Dat {
77 let mut map = DaticleMap::new();
78 map.insert(dat!(KEY_PK), Dat::BU8(self.pk.clone()));
79 map.insert(dat!(KEY_SK), Dat::BU8(self.sk.clone()));
80 Dat::Map(map)
81 }
82
83 /// Reads a pair from the map that carries it, naming the file it came from.
84 pub fn from_dat(
85 d: &Dat,
86 path: &Path,
87 )
88 -> Outcome<Self>
89 {
90 Ok(Self {
91 pk: res!(bytes(d, KEY_PK, path)),
92 sk: res!(bytes(d, KEY_SK, path)),
93 })
94 }
95}
96
97/// Signs a message with a pair, under the one scheme this version signs with.
98///
99/// A document's signature is over the envelope's signing input and is [`doc`](crate::doc)'s business.
100/// This is for the OTHER things a holder of a key may need to put their name to -- a declaration about
101/// the key itself, a statement carried beside a document rather than inside one -- so that they are
102/// signed by the same scheme, and checked by the same code, as a document is. A second signing path
103/// would be a second place for a scheme mismatch to hide.
104pub fn sign(
105 pair: &KeyPair,
106 msg: &[u8],
107)
108 -> Outcome<Vec<u8>>
109{
110 let signer = res!(pair.signer());
111 Ok(res!(signer.sign(msg)))
112}
113
114/// Whether a signature over a message is the one that public key would make.
115///
116/// The counterpart of [`sign`], and the same check `doc::verify` runs over an envelope: the key's
117/// length is held to the scheme's before anything else, because a key of the wrong length is a
118/// malformed input rather than a bad signature and the two want different words. A false answer is not
119/// an error -- a signature that is not this key's is a fact, and the caller decides what to do about
120/// it -- while a key that could not be read at all is.
121pub fn verify(
122 pk: &[u8],
123 msg: &[u8],
124 sig: &[u8],
125)
126 -> Outcome<bool>
127{
128 if pk.len() != SignatureScheme::ED25519_PK_LEN {
129 return Err(err!(
130 "An {} public key is {} bytes, and this one is {}.",
131 SCHEME_ED25519, SignatureScheme::ED25519_PK_LEN, pk.len();
132 Invalid, Input, Mismatch));
133 }
134 let verifier = res!(SignatureScheme::empty_ed25519().set_public_key(Some(pk)));
135 Ok(res!(verifier.verify(msg, sig)))
136}
137
138/// Reads a key file, taking the named entry if the file holds several pairs.
139pub fn load(
140 path: &Path,
141 entry: Option<&str>,
142)
143 -> Outcome<KeyPair>
144{
145 let src = match fs::read_to_string(path) {
146 Ok(src) => src,
147 Err(e) => return Err(err!(e,
148 "Could not read the key file {}.", path.display();
149 IO, File)),
150 };
151 let d = match text::decode_plain(&src) {
152 Ok(d) => d,
153 Err(e) => return Err(err!(e,
154 "The key file {} is not readable JDAT.", path.display();
155 Invalid, Input, Decode)),
156 };
157
158 // The scheme is named, never assumed: a key file for a scheme this version does not sign with is
159 // refused here rather than producing a signature nothing can check.
160 let scheme = res!(string(&d, KEY_SCHEME, path));
161 if scheme != SCHEME_ED25519 {
162 return Err(err!(
163 "The key file {} names the signature scheme '{}'; v0 signs with {}.",
164 path.display(), scheme, SCHEME_ED25519;
165 Invalid, Input, Unimplemented));
166 }
167
168 match entry {
169 None => KeyPair::from_dat(&d, path),
170 Some(name) => {
171 let inner = match &d {
172 Dat::Map(map) => match map.get(&dat!(name)) {
173 Some(inner) => inner.clone(),
174 None => return Err(err!(
175 "The key file {} carries no entry '{}'. It holds: {}.",
176 path.display(), name, entries(&d);
177 Invalid, Input, Missing)),
178 },
179 d => return Err(err!(
180 "The key file {} is a {:?}; a key file is a map.", path.display(), d.kind();
181 Invalid, Input)),
182 };
183 KeyPair::from_dat(&inner, path)
184 },
185 }
186}
187
188/// Writes a key file, readable only by its owner where the platform says so.
189pub fn save(
190 pair: &KeyPair,
191 path: &Path,
192)
193 -> Outcome<()>
194{
195 let mut map = DaticleMap::new();
196 map.insert(dat!(KEY_SCHEME), Dat::Str(SCHEME_ED25519.to_string()));
197 map.insert(dat!(KEY_PK), Dat::BU8(pair.pk.clone()));
198 map.insert(dat!(KEY_SK), Dat::BU8(pair.sk.clone()));
199 let src = res!(text::encode_plain(&Dat::Map(map)));
200 if let Some(dir) = path.parent() {
201 if !dir.as_os_str().is_empty() {
202 match fs::create_dir_all(dir) {
203 Ok(()) => (),
204 Err(e) => return Err(err!(e,
205 "Could not make the directory {} for the key file.", dir.display();
206 IO, File)),
207 }
208 }
209 }
210 // Written atomically at 0600 whatever the umask: a secret key readable
211 // by the machine, even briefly, is not a secret key.
212 res!(core_file::save_secret(path, src.as_bytes()));
213 Ok(())
214}
215
216/// The keys a key file carries, listed for an error message that must name what it did not find.
217fn entries(d: &Dat) -> String {
218 let map = match d {
219 Dat::Map(map) => map,
220 _ => return fmt!("nothing"),
221 };
222 let mut s = String::new();
223 for (k, _) in map {
224 if let Dat::Str(name) = k {
225 if !s.is_empty() {
226 s.push_str(", ");
227 }
228 s.push_str(&fmt!("'{}'", name));
229 }
230 }
231 if s.is_empty() {
232 s.push_str("nothing");
233 }
234 s
235}
236
237/// The string under a key of a key file.
238fn string(
239 d: &Dat,
240 key: &str,
241 path: &Path,
242)
243 -> Outcome<String>
244{
245 match res!(get(d, key, path)) {
246 Dat::Str(s) => Ok(s.clone()),
247 v => Err(err!(
248 "The key file {} carries a {:?} under '{}', where a str belongs.",
249 path.display(), v.kind(), key;
250 Invalid, Input, Mismatch)),
251 }
252}
253
254/// The raw bytes under a key of a key file.
255fn bytes(
256 d: &Dat,
257 key: &str,
258 path: &Path,
259)
260 -> Outcome<Vec<u8>>
261{
262 match res!(get(d, key, path)) {
263 Dat::BU8(v) => Ok(v.clone()),
264 v => Err(err!(
265 "The key file {} carries a {:?} under '{}', where a bu8 of raw key bytes belongs.",
266 path.display(), v.kind(), key;
267 Invalid, Input, Mismatch)),
268 }
269}
270
271/// The value under a key of a key file, or an error naming the file and the key.
272fn get<'a>(
273 d: &'a Dat,
274 key: &str,
275 path: &Path,
276)
277 -> Outcome<&'a Dat>
278{
279 match d {
280 Dat::Map(map) => match map.get(&dat!(key)) {
281 Some(v) => Ok(v),
282 None => Err(err!(
283 "The key file {} is missing the required key '{}'.", path.display(), key;
284 Invalid, Input, Missing)),
285 },
286 d => Err(err!(
287 "The key file {} holds a {:?}; a key file is a map carrying '{}', '{}' and '{}'.",
288 path.display(), d.kind(), KEY_SCHEME, KEY_PK, KEY_SK;
289 Invalid, Input)),
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 /// A key survives a trip through a file, and the pair that comes back signs as the one that went
298 /// in.
299 #[test]
300 fn test_key_file_round_trip_00() -> Outcome<()> {
301 let dir = std::env::temp_dir().join(fmt!("sbj_key_{}", std::process::id()));
302 let path = dir.join("key.jdat");
303 let pair = res!(KeyPair::generate());
304 res!(save(&pair, &path));
305
306 let read = res!(load(&path, None));
307 assert_eq!(read.pk, pair.pk, "The public key did not survive the file.");
308 assert_eq!(read.sk, pair.sk, "The secret key did not survive the file.");
309
310 // The pair that came back is the pair that signs.
311 let signer = res!(read.signer());
312 let sig = res!(signer.sign(b"the hash is the address"));
313 assert!(res!(signer.verify(b"the hash is the address", &sig)),
314 "The key that came back did not sign.");
315
316 match fs::remove_dir_all(&dir) {
317 Ok(()) => (),
318 Err(e) => return Err(err!(e, "Could not clean up {}.", dir.display(); IO, File)),
319 }
320 Ok(())
321 }
322
323 /// A key file that names a scheme this version does not sign with is refused, naming it.
324 #[test]
325 fn test_a_foreign_scheme_is_refused_01() -> Outcome<()> {
326 let dir = std::env::temp_dir().join(fmt!("sbj_key_foreign_{}", std::process::id()));
327 let path = dir.join("key.jdat");
328 let pair = res!(KeyPair::generate());
329 res!(save(&pair, &path));
330 let src = res!(fs::read_to_string(&path), IO, File);
331 let bad = src.replace(SCHEME_ED25519, "rsa");
332 res!(fs::write(&path, &bad), IO, File);
333
334 match load(&path, None) {
335 Ok(_) => return Err(err!("A key file naming RSA was read."; Test, Invalid)),
336 Err(e) => {
337 let msg = fmt!("{}", e);
338 assert!(msg.contains("rsa"), "The refusal should name the scheme: {}", msg);
339 },
340 }
341 match fs::remove_dir_all(&dir) {
342 Ok(()) => (),
343 Err(e) => return Err(err!(e, "Could not clean up {}.", dir.display(); IO, File)),
344 }
345 Ok(())
346 }
347}