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 | |
| 12 | use crate::text; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::{ |
| 15 | prelude::*, |
| 16 | file as core_file, |
| 17 | }; |
| 18 | use oxedyne_fe2o3_crypto::sign::SignatureScheme; |
| 19 | use oxedyne_fe2o3_iop_crypto::{ |
| 20 | keys::KeyManager, |
| 21 | sign::Signer, |
| 22 | }; |
| 23 | use oxedyne_fe2o3_jdat::prelude::*; |
| 24 | |
| 25 | use std::{ |
| 26 | fs, |
| 27 | path::Path, |
| 28 | }; |
| 29 | |
| 30 | /// The name the key file gives the v0 signature scheme. |
| 31 | pub const SCHEME_ED25519: &'static str = "ed25519"; |
| 32 | |
| 33 | /// The key-file key naming the signature scheme. |
| 34 | pub const KEY_SCHEME: &'static str = "scheme"; |
| 35 | /// The key-file key carrying the public key. |
| 36 | pub const KEY_PK: &'static str = "pk"; |
| 37 | /// The key-file key carrying the secret key. |
| 38 | pub const KEY_SK: &'static str = "sk"; |
| 39 | |
| 40 | /// An Ed25519 key pair, held as raw bytes. |
| 41 | #[derive(Clone, Debug)] |
| 42 | pub 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 | |
| 49 | impl 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. |
| 104 | pub 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. |
| 121 | pub 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. |
| 139 | pub 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. |
| 189 | pub 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. |
| 217 | fn 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. |
| 238 | fn 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. |
| 255 | fn 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. |
| 272 | fn 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)] |
| 294 | mod 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 | } |