oxedyne/fe2o3/fe2o3_net/src/presentation/shape.rs
36.5 KiB, 1 run
created by r1870400018:61154, 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 | use oxedyne_fe2o3_core::prelude::*; |
| 2 | use oxedyne_fe2o3_crypto::linkring; |
| 3 | use oxedyne_fe2o3_hash::sha256; |
| 4 | use oxedyne_fe2o3_jdat::{ |
| 5 | prelude::*, |
| 6 | bdat::DecodeLimits, |
| 7 | }; |
| 8 | use oxedyne_fe2o3_text::base64; |
| 9 | |
| 10 | |
| 11 | // Protocol words |
| 12 | pub const V_REQUEST: &str = "present-req/1"; |
| 13 | pub const V_PRESENTATION: &str = "present/1"; |
| 14 | pub const V_HEAD: &str = "head/1"; |
| 15 | pub const V_INVOICE: &str = "invoice/1"; |
| 16 | pub const V_SETTLEMENT: &str = "settle/1"; |
| 17 | pub const SCOPE_PREFIX: &str = "present/1:"; |
| 18 | pub const ALG_RING: &str = linkring::ALG; |
| 19 | |
| 20 | // Clocks, in seconds |
| 21 | pub const T_G: u64 = 300; // freshness of a head, a presentation and a nonce |
| 22 | pub const HEAD_LEAD: u64 = 60; // how far a head may postdate its presentation |
| 23 | pub const INVOICE_LIFE: u64 = 3_600; // longest an invoice may run |
| 24 | |
| 25 | // Sizes |
| 26 | pub const NONCE_LEN: usize = 32; |
| 27 | pub const KEY_LEN: usize = 32; |
| 28 | pub const SIG_LEN: usize = 64; |
| 29 | pub const SALT_LEN: usize = 16; |
| 30 | pub const INVOICE_NONCE_LEN: usize = 16; |
| 31 | pub const KEY_ID_CHARS: usize = 10; |
| 32 | pub const MEMO_MAX_CHARS: usize = 64; |
| 33 | pub const WORD_MAX_CHARS: usize = 32; |
| 34 | pub const WORDS_MAX: usize = 16; |
| 35 | pub const JSON_MAX_BYTES: usize = 64 * 1024; |
| 36 | pub const MAX_INT: u64 = 9_007_199_254_740_991; // 2^53 - 1 |
| 37 | |
| 38 | const JSON_MAX_DEPTH: usize = 16; // value nesting a parse descends to |
| 39 | |
| 40 | // Members, by shape |
| 41 | const REQUEST_MEMBERS: [&str; 8] = |
| 42 | ["v", "rp_id", "nonce", "modes", "predicates", "invoice", "return_to", "exp"]; |
| 43 | const REQUEST_RESERVED: [&str; 2] = ["org", "osig"]; // ignored until origins are bound to names |
| 44 | const NAMED_MEMBERS: [&str; 10] = |
| 45 | ["v", "mode", "rp_id", "nonce", "sub", "pub", "predicates", "head", "ts", "sig"]; |
| 46 | const PAIRWISE_MEMBERS: [&str; 11] = |
| 47 | ["v", "mode", "rp_id", "nonce", "sub", "tag", "predicates", "head", "ts", "proof", "sig"]; |
| 48 | const PROOF_MEMBERS: [&str; 2] = ["alg", "body"]; |
| 49 | const HEAD_MEMBERS: [&str; 11] = [ |
| 50 | "v", "head", "epoch", "prev", "ts", "salt", "members", "ring_n", "ring_digest", "signer", "sig", |
| 51 | ]; |
| 52 | const INVOICE_MEMBERS: [&str; 9] = |
| 53 | ["v", "rp_id", "payee", "account", "oxes", "memo", "nonce", "expires", "ts"]; |
| 54 | const SETTLEMENT_MEMBERS: [&str; 7] = ["v", "invoice", "entry", "oxes", "ts", "signer", "sig"]; |
| 55 | |
| 56 | |
| 57 | // ── Rules ─────────────────────────────────────────────────────────────────── |
| 58 | |
| 59 | /// Checks that `origin` is an origin in RFC 6454 ASCII serialisation, spelt the |
| 60 | /// one way a verifier compares it, byte for byte: `scheme "://" host [":" port]`, |
| 61 | /// scheme and host lowercase, the default port omitted, and no path, trailing |
| 62 | /// slash or user information. The scheme is `https`, or `http` with a host of |
| 63 | /// `localhost` or `127.0.0.1` for development. An IPv6 literal is not accepted, |
| 64 | /// and an IPv4 address only as a dotted quad. |
| 65 | pub fn check_origin(origin: &str) -> Outcome<()> { |
| 66 | let (scheme, rest) = match origin.split_once("://") { |
| 67 | Some(parts) => parts, |
| 68 | None => return Err(err!( |
| 69 | "The origin '{}' has no '://' after its scheme.", origin; |
| 70 | Invalid, Input)), |
| 71 | }; |
| 72 | let default_port = match scheme { |
| 73 | "https" => "443", |
| 74 | "http" => "80", |
| 75 | _ => return Err(err!( |
| 76 | "The origin '{}' has scheme '{}', where 'https' is required ('http' only for \ |
| 77 | localhost).", origin, scheme; |
| 78 | Invalid, Input)), |
| 79 | }; |
| 80 | let (host, port) = match rest.rsplit_once(':') { |
| 81 | Some((host, port)) => (host, Some(port)), |
| 82 | None => (rest, None), |
| 83 | }; |
| 84 | if host.is_empty() || host.len() > 253 { |
| 85 | return Err(err!( |
| 86 | "The origin '{}' has a host of {} characters, where 1 to 253 are allowed.", |
| 87 | origin, host.len(); |
| 88 | Invalid, Input, Size)); |
| 89 | } |
| 90 | // Lowercase letters, digits, hyphens and dots only, which also refuses a |
| 91 | // path, a slash, user information, a query and an upper case letter. |
| 92 | if let Some(c) = host.chars().find(|c| !(c.is_ascii_lowercase() |
| 93 | || c.is_ascii_digit() || *c == '-' || *c == '.')) |
| 94 | { |
| 95 | return Err(err!( |
| 96 | "The origin '{}' has '{}' in its host, which a serialised origin never \ |
| 97 | carries there.", origin, c.escape_default(); |
| 98 | Invalid, Input)); |
| 99 | } |
| 100 | for label in host.split('.') { |
| 101 | if label.is_empty() || label.len() > 63 || label.starts_with('-') || label.ends_with('-') { |
| 102 | return Err(err!( |
| 103 | "The origin '{}' has the host label '{}', which is empty, longer than 63 \ |
| 104 | characters, or begins or ends with a hyphen.", origin, label; |
| 105 | Invalid, Input)); |
| 106 | } |
| 107 | } |
| 108 | // A browser reads a host whose last label is a number, decimal or hex, as |
| 109 | // an IPv4 address and serialises it as a dotted quad, so `https://127.1` is |
| 110 | // `https://127.0.0.1` there (WHATWG URL, "ends in a number"). |
| 111 | if ends_in_a_number(host) && !is_dotted_quad(host) { |
| 112 | return Err(err!( |
| 113 | "The origin '{}' has a host that a browser reads as an IPv4 address, and \ |
| 114 | serialises otherwise than as written; only a dotted quad is.", origin; |
| 115 | Invalid, Input)); |
| 116 | } |
| 117 | if scheme == "http" && host != "localhost" && host != "127.0.0.1" { |
| 118 | return Err(err!( |
| 119 | "The origin '{}' is plain http on a host other than localhost or 127.0.0.1.", |
| 120 | origin; |
| 121 | Invalid, Input, Security)); |
| 122 | } |
| 123 | if let Some(port) = port { |
| 124 | // Digits only, since `parse` would also take a leading '+'. |
| 125 | let digits = !port.is_empty() && port.len() <= 5 && port.bytes().all(|b| b.is_ascii_digit()); |
| 126 | let number = match port.parse::<u32>() { |
| 127 | Ok(n) if digits => n, |
| 128 | _ => 0, |
| 129 | }; |
| 130 | if port.starts_with('0') || number == 0 || number > 65_535 { |
| 131 | return Err(err!( |
| 132 | "The origin '{}' has the port '{}', which is not a number from 1 to 65535 \ |
| 133 | without leading zeros.", origin, port; |
| 134 | Invalid, Input)); |
| 135 | } |
| 136 | if port == default_port { |
| 137 | return Err(err!( |
| 138 | "The origin '{}' names its scheme's default port, which a serialised \ |
| 139 | origin omits.", origin; |
| 140 | Invalid, Input)); |
| 141 | } |
| 142 | } |
| 143 | Ok(()) |
| 144 | } |
| 145 | |
| 146 | /// Does the host's last label read as a number, all decimal digits or `0x` and |
| 147 | /// hex digits? |
| 148 | fn ends_in_a_number(host: &str) -> bool { |
| 149 | let last = match host.rsplit('.').next() { |
| 150 | Some(last) => last, |
| 151 | None => return false, |
| 152 | }; |
| 153 | if !last.is_empty() && last.bytes().all(|b| b.is_ascii_digit()) { |
| 154 | return true; |
| 155 | } |
| 156 | match last.strip_prefix("0x") { |
| 157 | Some(hex) => hex.bytes().all(|b| b.is_ascii_hexdigit()), |
| 158 | None => false, |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | /// Is the host four decimal numbers from 0 to 255, without leading zeros? |
| 163 | fn is_dotted_quad(host: &str) -> bool { |
| 164 | let parts: Vec<&str> = host.split('.').collect(); |
| 165 | parts.len() == 4 && parts.iter().all(|p| { |
| 166 | !p.is_empty() |
| 167 | && p.bytes().all(|b| b.is_ascii_digit()) |
| 168 | && (p.len() == 1 || !p.starts_with('0')) |
| 169 | && matches!(p.parse::<u16>(), Ok(n) if n <= 255) |
| 170 | }) |
| 171 | } |
| 172 | |
| 173 | /// The key id a public key earns: the first ten lowercase hex characters of |
| 174 | /// SHA-256 over its raw bytes. |
| 175 | pub fn key_id(public: &[u8]) -> String { |
| 176 | let digest = sha256::digest(public); |
| 177 | let mut id = String::with_capacity(KEY_ID_CHARS); |
| 178 | for b in &digest[..KEY_ID_CHARS / 2] { |
| 179 | id.push_str(&fmt!("{:02x}", b)); |
| 180 | } |
| 181 | id |
| 182 | } |
| 183 | |
| 184 | /// Is `s` a key id, ten lowercase hex characters? |
| 185 | pub fn is_key_id(s: &str) -> bool { |
| 186 | s.len() == KEY_ID_CHARS && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) |
| 187 | } |
| 188 | |
| 189 | /// Is `s` a predicate word, 1 to 32 lowercase ASCII letters, digits and |
| 190 | /// underscores? |
| 191 | pub fn is_word(s: &str) -> bool { |
| 192 | !s.is_empty() |
| 193 | && s.len() <= WORD_MAX_CHARS |
| 194 | && s.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') |
| 195 | } |
| 196 | |
| 197 | /// The scope a pairwise proof for `rp_id` links under: `"present/1:"` and the |
| 198 | /// origin, as UTF-8 bytes. One human has one tag per scope. |
| 199 | pub fn scope(rp_id: &str) -> Vec<u8> { |
| 200 | let mut s = Vec::with_capacity(SCOPE_PREFIX.len() + rp_id.len()); |
| 201 | s.extend_from_slice(SCOPE_PREFIX.as_bytes()); |
| 202 | s.extend_from_slice(rp_id.as_bytes()); |
| 203 | s |
| 204 | } |
| 205 | |
| 206 | |
| 207 | // ── Modes ─────────────────────────────────────────────────────────────────── |
| 208 | |
| 209 | /// How a member presents: under a public name, or under a pseudonym and a tag |
| 210 | /// that only this relying party sees. |
| 211 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 212 | pub enum Mode { |
| 213 | Named, |
| 214 | Pairwise, |
| 215 | } |
| 216 | |
| 217 | impl Mode { |
| 218 | pub fn word(&self) -> &'static str { |
| 219 | match self { |
| 220 | Self::Named => "named", |
| 221 | Self::Pairwise => "pairwise", |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | pub fn from_word(s: &str) -> Option<Self> { |
| 226 | match s { |
| 227 | "named" => Some(Self::Named), |
| 228 | "pairwise" => Some(Self::Pairwise), |
| 229 | _ => None, |
| 230 | } |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | /// The modes a request accepts. Pairwise is always among them, so a relying |
| 235 | /// party may accept a public name but cannot demand one. |
| 236 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| 237 | pub enum Accept { |
| 238 | Pairwise, // wire ["pairwise"] |
| 239 | PairwiseOrNamed, // wire ["pairwise", "named"] |
| 240 | } |
| 241 | |
| 242 | impl Accept { |
| 243 | /// Does a request carrying this accept a presentation in `mode`? |
| 244 | pub fn admits(&self, mode: Mode) -> bool { |
| 245 | match (self, mode) { |
| 246 | (_, Mode::Pairwise) => true, |
| 247 | (Self::PairwiseOrNamed, Mode::Named) => true, |
| 248 | (Self::Pairwise, Mode::Named) => false, |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | fn to_dat(&self) -> Dat { |
| 253 | match self { |
| 254 | Self::Pairwise => listdat![Mode::Pairwise.word()], |
| 255 | Self::PairwiseOrNamed => listdat![Mode::Pairwise.word(), Mode::Named.word()], |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 260 | let words: Vec<&str> = match dat { |
| 261 | Dat::List(list) => list.iter().filter_map(|d| match d { |
| 262 | Dat::Str(s) => Some(s.as_str()), |
| 263 | _ => None, |
| 264 | }).collect(), |
| 265 | _ => Vec::new(), |
| 266 | }; |
| 267 | match (dat, words.as_slice()) { |
| 268 | (Dat::List(list), ["pairwise"]) if list.len() == 1 => Ok(Self::Pairwise), |
| 269 | (Dat::List(list), ["pairwise", "named"]) if list.len() == 2 => Ok(Self::PairwiseOrNamed), |
| 270 | _ => Err(err!( |
| 271 | "A request's modes are [\"pairwise\"] or [\"pairwise\", \"named\"], not {:?}.", |
| 272 | dat; |
| 273 | Invalid, Input)), |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | |
| 279 | // ── Request ───────────────────────────────────────────────────────────────── |
| 280 | |
| 281 | /// A relying party's challenge to one browser session: its origin, a nonce, |
| 282 | /// what it accepts and asks, and when the challenge lapses. |
| 283 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 284 | pub struct Request { |
| 285 | pub rp_id: String, |
| 286 | pub nonce: [u8; NONCE_LEN], |
| 287 | pub accept: Accept, |
| 288 | pub predicates: Vec<String>, |
| 289 | pub invoice: Option<Invoice>, |
| 290 | pub return_to: Option<String>, // for the redirect route only, a URL on `rp_id` |
| 291 | pub exp: u64, |
| 292 | } |
| 293 | |
| 294 | impl Request { |
| 295 | |
| 296 | /// Checks what the shape alone cannot: the origin, the words, the return |
| 297 | /// URL and any invoice. |
| 298 | pub fn check(&self) -> Outcome<()> { |
| 299 | res!(check_origin(&self.rp_id)); |
| 300 | res!(check_words(&self.predicates, "request")); |
| 301 | if let Some(url) = &self.return_to { |
| 302 | let on_origin = url.starts_with(&self.rp_id) |
| 303 | && url.as_bytes().get(self.rp_id.len()) == Some(&b'/'); |
| 304 | if !on_origin || url.chars().any(|c| c == '#' || c.is_whitespace() || c.is_control()) { |
| 305 | return Err(err!( |
| 306 | "The return URL '{}' is not a URL on {} without a fragment.", url, self.rp_id; |
| 307 | Invalid, Input)); |
| 308 | } |
| 309 | } |
| 310 | if let Some(invoice) = &self.invoice { |
| 311 | res!(invoice.check()); |
| 312 | if invoice.rp_id != self.rp_id { |
| 313 | return Err(err!( |
| 314 | "The invoice was issued by {}, and the request by {}.", |
| 315 | invoice.rp_id, self.rp_id; |
| 316 | Invalid, Input, Mismatch)); |
| 317 | } |
| 318 | } |
| 319 | Ok(()) |
| 320 | } |
| 321 | |
| 322 | pub fn to_dat(&self) -> Dat { |
| 323 | let mut m = DaticleMap::new(); |
| 324 | m.insert(dat!("v"), dat!(V_REQUEST)); |
| 325 | m.insert(dat!("rp_id"), dat!(self.rp_id.clone())); |
| 326 | m.insert(dat!("nonce"), dat!(base64::encode_url(&self.nonce))); |
| 327 | m.insert(dat!("modes"), self.accept.to_dat()); |
| 328 | m.insert(dat!("predicates"), words_dat(&self.predicates)); |
| 329 | if let Some(invoice) = &self.invoice { |
| 330 | m.insert(dat!("invoice"), invoice.to_dat()); |
| 331 | } |
| 332 | if let Some(url) = &self.return_to { |
| 333 | m.insert(dat!("return_to"), dat!(url.clone())); |
| 334 | } |
| 335 | m.insert(dat!("exp"), Dat::U64(self.exp)); |
| 336 | Dat::Map(m) |
| 337 | } |
| 338 | |
| 339 | pub fn to_json(&self) -> Outcome<String> { |
| 340 | self.to_dat().json_canonical() |
| 341 | } |
| 342 | |
| 343 | /// Reads a request strictly: an unknown member is refused, save `org` and |
| 344 | /// `osig`, which are reserved and ignored. |
| 345 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 346 | let o = res!(Obj::of(dat, "request")); |
| 347 | res!(o.only(&REQUEST_MEMBERS, &REQUEST_RESERVED)); |
| 348 | res!(o.version(V_REQUEST)); |
| 349 | let invoice = match o.get("invoice") { |
| 350 | Some(d) => Some(res!(Invoice::from_dat(d))), |
| 351 | None => None, |
| 352 | }; |
| 353 | let return_to = match o.get("return_to") { |
| 354 | Some(_) => Some(res!(o.text("return_to"))), |
| 355 | None => None, |
| 356 | }; |
| 357 | let req = Self { |
| 358 | rp_id: res!(o.text("rp_id")), |
| 359 | nonce: res!(o.bytes::<NONCE_LEN>("nonce")), |
| 360 | accept: res!(Accept::from_dat(res!(o.must("modes")))), |
| 361 | predicates: res!(o.words("predicates")), |
| 362 | invoice, |
| 363 | return_to, |
| 364 | exp: res!(o.uint("exp")), |
| 365 | }; |
| 366 | res!(req.check()); |
| 367 | Ok(req) |
| 368 | } |
| 369 | |
| 370 | pub fn parse(json: &str) -> Outcome<Self> { |
| 371 | Self::from_dat(&res!(decode_json(json, "request"))) |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | |
| 376 | // ── Presentation ──────────────────────────────────────────────────────────── |
| 377 | |
| 378 | /// Who presents, with what the mode needs. |
| 379 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 380 | pub enum Subject { |
| 381 | Named { |
| 382 | id: String, // the key id of `key`, on the wire as `sub` |
| 383 | key: [u8; KEY_LEN], // Ed25519, on the wire as `pub` |
| 384 | }, |
| 385 | Pairwise { |
| 386 | key: [u8; KEY_LEN], // Ed25519 pseudonym for this relying party, `sub` |
| 387 | tag: [u8; KEY_LEN], // linking tag under this relying party's scope |
| 388 | proof: Proof, |
| 389 | }, |
| 390 | } |
| 391 | |
| 392 | /// A ring proof, named by its algorithm. Its bytes are the algorithm's own. |
| 393 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 394 | pub struct Proof { |
| 395 | pub alg: String, |
| 396 | pub body: Vec<u8>, |
| 397 | } |
| 398 | |
| 399 | /// A member's answer to a request. |
| 400 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 401 | pub struct Presentation { |
| 402 | pub rp_id: String, |
| 403 | pub nonce: [u8; NONCE_LEN], |
| 404 | pub subject: Subject, |
| 405 | pub predicates: Vec<String>, |
| 406 | pub head: [u8; 32], |
| 407 | pub ts: u64, |
| 408 | pub sig: [u8; SIG_LEN], |
| 409 | } |
| 410 | |
| 411 | impl Presentation { |
| 412 | |
| 413 | pub fn mode(&self) -> Mode { |
| 414 | match self.subject { |
| 415 | Subject::Named { .. } => Mode::Named, |
| 416 | Subject::Pairwise { .. } => Mode::Pairwise, |
| 417 | } |
| 418 | } |
| 419 | |
| 420 | fn signed_map(&self) -> DaticleMap { |
| 421 | let mut m = DaticleMap::new(); |
| 422 | m.insert(dat!("v"), dat!(V_PRESENTATION)); |
| 423 | m.insert(dat!("mode"), dat!(self.mode().word())); |
| 424 | m.insert(dat!("rp_id"), dat!(self.rp_id.clone())); |
| 425 | m.insert(dat!("nonce"), dat!(base64::encode_url(&self.nonce))); |
| 426 | match &self.subject { |
| 427 | Subject::Named { id, key } => { |
| 428 | m.insert(dat!("sub"), dat!(id.clone())); |
| 429 | m.insert(dat!("pub"), dat!(base64::encode_url(key))); |
| 430 | }, |
| 431 | Subject::Pairwise { key, tag, .. } => { |
| 432 | m.insert(dat!("sub"), dat!(base64::encode_url(key))); |
| 433 | m.insert(dat!("tag"), dat!(base64::encode_url(tag))); |
| 434 | }, |
| 435 | } |
| 436 | m.insert(dat!("predicates"), words_dat(&self.predicates)); |
| 437 | m.insert(dat!("head"), dat!(base64::encode_url(&self.head))); |
| 438 | m.insert(dat!("ts"), Dat::U64(self.ts)); |
| 439 | m |
| 440 | } |
| 441 | |
| 442 | /// The bytes the signature and a pairwise proof both cover: the canonical |
| 443 | /// JSON of every member but `proof` and `sig`, rebuilt from the parsed |
| 444 | /// values, so the sender's own spelling of the object cannot move a byte. |
| 445 | pub fn signed_bytes(&self) -> Outcome<Vec<u8>> { |
| 446 | Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes()) |
| 447 | } |
| 448 | |
| 449 | pub fn to_dat(&self) -> Dat { |
| 450 | let mut m = self.signed_map(); |
| 451 | if let Subject::Pairwise { proof, .. } = &self.subject { |
| 452 | m.insert(dat!("proof"), mapdat!{ |
| 453 | "alg" => proof.alg.clone(), |
| 454 | "body" => base64::encode_url(&proof.body), |
| 455 | }); |
| 456 | } |
| 457 | m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig))); |
| 458 | Dat::Map(m) |
| 459 | } |
| 460 | |
| 461 | pub fn to_json(&self) -> Outcome<String> { |
| 462 | self.to_dat().json_canonical() |
| 463 | } |
| 464 | |
| 465 | /// Reads a presentation strictly: exactly the members its mode carries, each |
| 466 | /// of its stated form. |
| 467 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 468 | let o = res!(Obj::of(dat, "presentation")); |
| 469 | res!(o.version(V_PRESENTATION)); |
| 470 | let mode = match Mode::from_word(&res!(o.text("mode"))) { |
| 471 | Some(mode) => mode, |
| 472 | None => return Err(err!( |
| 473 | "A presentation's mode is 'named' or 'pairwise'."; |
| 474 | Invalid, Input)), |
| 475 | }; |
| 476 | let subject = match mode { |
| 477 | Mode::Named => { |
| 478 | res!(o.only(&NAMED_MEMBERS, &[])); |
| 479 | let id = res!(o.text("sub")); |
| 480 | if !is_key_id(&id) { |
| 481 | return Err(err!( |
| 482 | "A named presentation's sub is a key id of ten lowercase hex \ |
| 483 | characters, not '{}'.", id; |
| 484 | Invalid, Input)); |
| 485 | } |
| 486 | Subject::Named { id, key: res!(o.bytes::<KEY_LEN>("pub")) } |
| 487 | }, |
| 488 | Mode::Pairwise => { |
| 489 | res!(o.only(&PAIRWISE_MEMBERS, &[])); |
| 490 | let p = res!(Obj::of(res!(o.must("proof")), "proof")); |
| 491 | res!(p.only(&PROOF_MEMBERS, &[])); |
| 492 | let body = res!(base64::decode_url(&res!(p.text("body")))); |
| 493 | if body.is_empty() { |
| 494 | return Err(err!("A proof's body is empty."; Invalid, Input, Missing)); |
| 495 | } |
| 496 | Subject::Pairwise { |
| 497 | key: res!(o.bytes::<KEY_LEN>("sub")), |
| 498 | tag: res!(o.bytes::<KEY_LEN>("tag")), |
| 499 | proof: Proof { alg: res!(p.text("alg")), body }, |
| 500 | } |
| 501 | }, |
| 502 | }; |
| 503 | Ok(Self { |
| 504 | rp_id: res!(o.text("rp_id")), |
| 505 | nonce: res!(o.bytes::<NONCE_LEN>("nonce")), |
| 506 | subject, |
| 507 | predicates: res!(o.words("predicates")), |
| 508 | head: res!(o.bytes::<32>("head")), |
| 509 | ts: res!(o.uint("ts")), |
| 510 | sig: res!(o.bytes::<SIG_LEN>("sig")), |
| 511 | }) |
| 512 | } |
| 513 | |
| 514 | pub fn parse(json: &str) -> Outcome<Self> { |
| 515 | Self::from_dat(&res!(decode_json(json, "presentation"))) |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | |
| 520 | // ── Head ──────────────────────────────────────────────────────────────────── |
| 521 | |
| 522 | /// A signed point in a network's history that a presentation is made against: |
| 523 | /// the member set's size and ring at a moment, under an issuer's key. |
| 524 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 525 | pub struct Head { |
| 526 | pub id: [u8; 32], // SHA-256 of the signed bytes, on the wire as `head` |
| 527 | pub epoch: u64, |
| 528 | pub prev: Option<[u8; 32]>, |
| 529 | pub ts: u64, |
| 530 | pub salt: [u8; SALT_LEN], |
| 531 | pub members: u64, |
| 532 | pub ring_n: u64, |
| 533 | pub ring_digest: [u8; 32], // SHA-256 of the ring list |
| 534 | pub signer: [u8; KEY_LEN], // Ed25519 |
| 535 | pub sig: [u8; SIG_LEN], |
| 536 | } |
| 537 | |
| 538 | impl Head { |
| 539 | |
| 540 | fn signed_map(&self) -> DaticleMap { |
| 541 | let mut m = DaticleMap::new(); |
| 542 | m.insert(dat!("v"), dat!(V_HEAD)); |
| 543 | m.insert(dat!("epoch"), Dat::U64(self.epoch)); |
| 544 | m.insert(dat!("prev"), match &self.prev { |
| 545 | Some(prev) => dat!(base64::encode_url(prev)), |
| 546 | None => Dat::Empty, |
| 547 | }); |
| 548 | m.insert(dat!("ts"), Dat::U64(self.ts)); |
| 549 | m.insert(dat!("salt"), dat!(base64::encode_url(&self.salt))); |
| 550 | m.insert(dat!("members"), Dat::U64(self.members)); |
| 551 | m.insert(dat!("ring_n"), Dat::U64(self.ring_n)); |
| 552 | m.insert(dat!("ring_digest"), dat!(base64::encode_url(&self.ring_digest))); |
| 553 | m.insert(dat!("signer"), dat!(base64::encode_url(&self.signer))); |
| 554 | m |
| 555 | } |
| 556 | |
| 557 | /// The bytes the signature covers and the id hashes: the canonical JSON of |
| 558 | /// every member but `head` and `sig`, with `prev` a present null when there |
| 559 | /// is none. |
| 560 | pub fn signed_bytes(&self) -> Outcome<Vec<u8>> { |
| 561 | Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes()) |
| 562 | } |
| 563 | |
| 564 | /// The id this head's content earns, which its `id` must equal. |
| 565 | pub fn compute_id(&self) -> Outcome<[u8; 32]> { |
| 566 | Ok(sha256::digest(&res!(self.signed_bytes()))) |
| 567 | } |
| 568 | |
| 569 | pub fn to_dat(&self) -> Dat { |
| 570 | let mut m = self.signed_map(); |
| 571 | m.insert(dat!("head"), dat!(base64::encode_url(&self.id))); |
| 572 | m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig))); |
| 573 | Dat::Map(m) |
| 574 | } |
| 575 | |
| 576 | pub fn to_json(&self) -> Outcome<String> { |
| 577 | self.to_dat().json_canonical() |
| 578 | } |
| 579 | |
| 580 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 581 | let o = res!(Obj::of(dat, "head")); |
| 582 | res!(o.only(&HEAD_MEMBERS, &[])); |
| 583 | res!(o.version(V_HEAD)); |
| 584 | Ok(Self { |
| 585 | id: res!(o.bytes::<32>("head")), |
| 586 | epoch: res!(o.uint("epoch")), |
| 587 | prev: res!(o.bytes_or_null::<32>("prev")), |
| 588 | ts: res!(o.uint("ts")), |
| 589 | salt: res!(o.bytes::<SALT_LEN>("salt")), |
| 590 | members: res!(o.uint("members")), |
| 591 | ring_n: res!(o.uint("ring_n")), |
| 592 | ring_digest: res!(o.bytes::<32>("ring_digest")), |
| 593 | signer: res!(o.bytes::<KEY_LEN>("signer")), |
| 594 | sig: res!(o.bytes::<SIG_LEN>("sig")), |
| 595 | }) |
| 596 | } |
| 597 | |
| 598 | pub fn parse(json: &str) -> Outcome<Self> { |
| 599 | Self::from_dat(&res!(decode_json(json, "head"))) |
| 600 | } |
| 601 | } |
| 602 | |
| 603 | |
| 604 | // ── Invoice and settlement ────────────────────────────────────────────────── |
| 605 | |
| 606 | /// A relying party's bill to a member, paid once through the network's ledger. |
| 607 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 608 | pub struct Invoice { |
| 609 | pub rp_id: String, |
| 610 | pub payee: String, // key id of the name being paid |
| 611 | pub account: String, // key id of the account credited |
| 612 | pub amount: u64, // on the wire as `oxes`, the ledger's unit |
| 613 | pub memo: String, |
| 614 | pub nonce: [u8; INVOICE_NONCE_LEN], |
| 615 | pub expires: u64, |
| 616 | pub ts: u64, |
| 617 | } |
| 618 | |
| 619 | impl Invoice { |
| 620 | |
| 621 | /// Checks what the shape alone cannot. The memo is at most 64 characters |
| 622 | /// once trimmed and carries no control character. |
| 623 | pub fn check(&self) -> Outcome<()> { |
| 624 | res!(check_origin(&self.rp_id)); |
| 625 | if !is_key_id(&self.payee) || !is_key_id(&self.account) { |
| 626 | return Err(err!( |
| 627 | "An invoice's payee and account are key ids of ten lowercase hex \ |
| 628 | characters, not '{}' and '{}'.", self.payee, self.account; |
| 629 | Invalid, Input)); |
| 630 | } |
| 631 | if self.amount == 0 || self.amount > MAX_INT { |
| 632 | return Err(err!( |
| 633 | "An invoice's amount is from 1 to 2^53 - 1, not {}.", self.amount; |
| 634 | Invalid, Input)); |
| 635 | } |
| 636 | let chars = self.memo.trim().chars().count(); |
| 637 | if chars > MEMO_MAX_CHARS || self.memo.chars().any(|c| c.is_control()) { |
| 638 | return Err(err!( |
| 639 | "An invoice's memo is at most {} characters once trimmed, with no control \ |
| 640 | character; this one has {} characters.", MEMO_MAX_CHARS, chars; |
| 641 | Invalid, Input)); |
| 642 | } |
| 643 | if self.expires < self.ts || self.expires - self.ts > INVOICE_LIFE { |
| 644 | return Err(err!( |
| 645 | "An invoice expires no earlier than it was issued and at most {} s after, \ |
| 646 | not {} s from {} to {}.", INVOICE_LIFE, |
| 647 | self.expires as i128 - self.ts as i128, self.ts, self.expires; |
| 648 | Invalid, Input)); |
| 649 | } |
| 650 | Ok(()) |
| 651 | } |
| 652 | |
| 653 | pub fn to_dat(&self) -> Dat { |
| 654 | mapdat!{ |
| 655 | "v" => V_INVOICE, |
| 656 | "rp_id" => self.rp_id.clone(), |
| 657 | "payee" => self.payee.clone(), |
| 658 | "account" => self.account.clone(), |
| 659 | "oxes" => Dat::U64(self.amount), |
| 660 | "memo" => self.memo.clone(), |
| 661 | "nonce" => base64::encode_url(&self.nonce), |
| 662 | "expires" => Dat::U64(self.expires), |
| 663 | "ts" => Dat::U64(self.ts), |
| 664 | } |
| 665 | } |
| 666 | |
| 667 | pub fn to_json(&self) -> Outcome<String> { |
| 668 | self.to_dat().json_canonical() |
| 669 | } |
| 670 | |
| 671 | /// SHA-256 over the canonical JSON: what a payment's reference and a |
| 672 | /// settlement name. |
| 673 | pub fn id(&self) -> Outcome<[u8; 32]> { |
| 674 | Ok(sha256::digest(res!(self.to_json()).as_bytes())) |
| 675 | } |
| 676 | |
| 677 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 678 | let o = res!(Obj::of(dat, "invoice")); |
| 679 | res!(o.only(&INVOICE_MEMBERS, &[])); |
| 680 | res!(o.version(V_INVOICE)); |
| 681 | let invoice = Self { |
| 682 | rp_id: res!(o.text("rp_id")), |
| 683 | payee: res!(o.text("payee")), |
| 684 | account: res!(o.text("account")), |
| 685 | amount: res!(o.uint("oxes")), |
| 686 | memo: res!(o.text("memo")), |
| 687 | nonce: res!(o.bytes::<INVOICE_NONCE_LEN>("nonce")), |
| 688 | expires: res!(o.uint("expires")), |
| 689 | ts: res!(o.uint("ts")), |
| 690 | }; |
| 691 | res!(invoice.check()); |
| 692 | Ok(invoice) |
| 693 | } |
| 694 | |
| 695 | pub fn parse(json: &str) -> Outcome<Self> { |
| 696 | Self::from_dat(&res!(decode_json(json, "invoice"))) |
| 697 | } |
| 698 | } |
| 699 | |
| 700 | /// An issuer's signed word that an invoice was paid, naming no payer. |
| 701 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 702 | pub struct Settlement { |
| 703 | pub invoice: [u8; 32], // the invoice's id |
| 704 | pub entry: [u8; 32], // the ledger entry that paid it |
| 705 | pub amount: u64, // on the wire as `oxes` |
| 706 | pub ts: u64, |
| 707 | pub signer: [u8; KEY_LEN], // Ed25519 |
| 708 | pub sig: [u8; SIG_LEN], |
| 709 | } |
| 710 | |
| 711 | impl Settlement { |
| 712 | |
| 713 | fn signed_map(&self) -> DaticleMap { |
| 714 | let mut m = DaticleMap::new(); |
| 715 | m.insert(dat!("v"), dat!(V_SETTLEMENT)); |
| 716 | m.insert(dat!("invoice"), dat!(base64::encode_url(&self.invoice))); |
| 717 | m.insert(dat!("entry"), dat!(base64::encode_url(&self.entry))); |
| 718 | m.insert(dat!("oxes"), Dat::U64(self.amount)); |
| 719 | m.insert(dat!("ts"), Dat::U64(self.ts)); |
| 720 | m.insert(dat!("signer"), dat!(base64::encode_url(&self.signer))); |
| 721 | m |
| 722 | } |
| 723 | |
| 724 | /// The bytes the signature covers: the canonical JSON of every member but |
| 725 | /// `sig`. |
| 726 | pub fn signed_bytes(&self) -> Outcome<Vec<u8>> { |
| 727 | Ok(res!(Dat::Map(self.signed_map()).json_canonical()).into_bytes()) |
| 728 | } |
| 729 | |
| 730 | pub fn to_dat(&self) -> Dat { |
| 731 | let mut m = self.signed_map(); |
| 732 | m.insert(dat!("sig"), dat!(base64::encode_url(&self.sig))); |
| 733 | Dat::Map(m) |
| 734 | } |
| 735 | |
| 736 | pub fn to_json(&self) -> Outcome<String> { |
| 737 | self.to_dat().json_canonical() |
| 738 | } |
| 739 | |
| 740 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 741 | let o = res!(Obj::of(dat, "settlement")); |
| 742 | res!(o.only(&SETTLEMENT_MEMBERS, &[])); |
| 743 | res!(o.version(V_SETTLEMENT)); |
| 744 | Ok(Self { |
| 745 | invoice: res!(o.bytes::<32>("invoice")), |
| 746 | entry: res!(o.bytes::<32>("entry")), |
| 747 | amount: res!(o.uint("oxes")), |
| 748 | ts: res!(o.uint("ts")), |
| 749 | signer: res!(o.bytes::<KEY_LEN>("signer")), |
| 750 | sig: res!(o.bytes::<SIG_LEN>("sig")), |
| 751 | }) |
| 752 | } |
| 753 | |
| 754 | pub fn parse(json: &str) -> Outcome<Self> { |
| 755 | Self::from_dat(&res!(decode_json(json, "settlement"))) |
| 756 | } |
| 757 | } |
| 758 | |
| 759 | |
| 760 | // ── Status ────────────────────────────────────────────────────────────────── |
| 761 | |
| 762 | /// What a named-mode status lookup reports: the key a name holds now, and |
| 763 | /// whether it is live. |
| 764 | #[derive(Clone, Debug, Eq, PartialEq)] |
| 765 | pub struct Status { |
| 766 | pub key: [u8; KEY_LEN], |
| 767 | pub live: bool, |
| 768 | } |
| 769 | |
| 770 | impl Status { |
| 771 | |
| 772 | /// Reads `pub` and `live` from a status document and nothing else, since |
| 773 | /// the document carries display fields a verifier has no use for. |
| 774 | pub fn from_dat(dat: &Dat) -> Outcome<Self> { |
| 775 | let o = res!(Obj::of(dat, "status")); |
| 776 | Ok(Self { |
| 777 | key: res!(o.bytes::<KEY_LEN>("pub")), |
| 778 | live: res!(o.flag("live")), |
| 779 | }) |
| 780 | } |
| 781 | |
| 782 | pub fn parse(json: &str) -> Outcome<Self> { |
| 783 | Self::from_dat(&res!(decode_json(json, "status"))) |
| 784 | } |
| 785 | } |
| 786 | |
| 787 | |
| 788 | // ── Reading ───────────────────────────────────────────────────────────────── |
| 789 | |
| 790 | /// Decodes a JSON document within the size and nesting these shapes need, as |
| 791 | /// RFC 8259 JSON and nothing else: none of JDAT's typed, hex or unquoted forms. |
| 792 | fn decode_json(json: &str, what: &str) -> Outcome<Dat> { |
| 793 | if json.len() > JSON_MAX_BYTES { |
| 794 | return Err(err!( |
| 795 | "A {} of {} bytes exceeds the {} bytes one may take.", what, json.len(), |
| 796 | JSON_MAX_BYTES; |
| 797 | Invalid, Input, Size)); |
| 798 | } |
| 799 | Ok(res!(Dat::decode_json_strict(json, &DecodeLimits::new(JSON_MAX_DEPTH, JSON_MAX_BYTES)))) |
| 800 | } |
| 801 | |
| 802 | fn words_dat(words: &[String]) -> Dat { |
| 803 | Dat::List(words.iter().map(|w| dat!(w.clone())).collect()) |
| 804 | } |
| 805 | |
| 806 | fn check_words(words: &[String], what: &str) -> Outcome<()> { |
| 807 | if words.len() > WORDS_MAX { |
| 808 | return Err(err!( |
| 809 | "A {} carries {} predicates, and at most {} are allowed.", what, words.len(), WORDS_MAX; |
| 810 | Invalid, Input, Size)); |
| 811 | } |
| 812 | for (i, w) in words.iter().enumerate() { |
| 813 | if !is_word(w) { |
| 814 | return Err(err!( |
| 815 | "A {}'s predicate '{}' is not a word of 1 to {} lowercase letters, digits \ |
| 816 | and underscores.", what, w, WORD_MAX_CHARS; |
| 817 | Invalid, Input)); |
| 818 | } |
| 819 | if words[..i].contains(w) { |
| 820 | return Err(err!( |
| 821 | "A {} names the predicate '{}' twice.", what, w; |
| 822 | Invalid, Input, Duplicate)); |
| 823 | } |
| 824 | } |
| 825 | Ok(()) |
| 826 | } |
| 827 | |
| 828 | /// The members of one JSON object, read strictly. |
| 829 | struct Obj<'a> { |
| 830 | map: &'a DaticleMap, |
| 831 | what: &'static str, // what the object is, for errors |
| 832 | } |
| 833 | |
| 834 | impl<'a> Obj<'a> { |
| 835 | |
| 836 | fn of(dat: &'a Dat, what: &'static str) -> Outcome<Self> { |
| 837 | match dat { |
| 838 | Dat::Map(map) => Ok(Self { map, what }), |
| 839 | other => Err(err!( |
| 840 | "A {} is a JSON object, not a {:?}.", what, other.kind(); |
| 841 | Invalid, Input, Decode)), |
| 842 | } |
| 843 | } |
| 844 | |
| 845 | /// Refuses any member outside `allowed`, save those in `ignored`. |
| 846 | fn only(&self, allowed: &[&str], ignored: &[&str]) -> Outcome<()> { |
| 847 | for key in self.map.keys() { |
| 848 | match key { |
| 849 | Dat::Str(s) if allowed.contains(&s.as_str()) || ignored.contains(&s.as_str()) => (), |
| 850 | other => return Err(err!( |
| 851 | "A {} has no member {:?}.", self.what, other; |
| 852 | Invalid, Input, Unknown)), |
| 853 | } |
| 854 | } |
| 855 | Ok(()) |
| 856 | } |
| 857 | |
| 858 | fn version(&self, v: &str) -> Outcome<()> { |
| 859 | let got = res!(self.text("v")); |
| 860 | if got != v { |
| 861 | return Err(err!( |
| 862 | "A {} carries v '{}', and this reads only '{}'.", self.what, got, v; |
| 863 | Invalid, Input, Version)); |
| 864 | } |
| 865 | Ok(()) |
| 866 | } |
| 867 | |
| 868 | fn get(&self, key: &str) -> Option<&'a Dat> { |
| 869 | self.map.get(&dat!(key)) |
| 870 | } |
| 871 | |
| 872 | fn must(&self, key: &str) -> Outcome<&'a Dat> { |
| 873 | match self.get(key) { |
| 874 | Some(d) => Ok(d), |
| 875 | None => Err(err!( |
| 876 | "A {} lacks its '{}' member.", self.what, key; |
| 877 | Invalid, Input, Missing)), |
| 878 | } |
| 879 | } |
| 880 | |
| 881 | fn text(&self, key: &str) -> Outcome<String> { |
| 882 | match res!(self.must(key)) { |
| 883 | Dat::Str(s) => Ok(s.clone()), |
| 884 | other => Err(err!( |
| 885 | "The '{}' of a {} is a string, not a {:?}.", key, self.what, other.kind(); |
| 886 | Invalid, Input, Mismatch)), |
| 887 | } |
| 888 | } |
| 889 | |
| 890 | fn flag(&self, key: &str) -> Outcome<bool> { |
| 891 | match res!(self.must(key)) { |
| 892 | Dat::Bool(b) => Ok(*b), |
| 893 | other => Err(err!( |
| 894 | "The '{}' of a {} is true or false, not a {:?}.", key, self.what, other.kind(); |
| 895 | Invalid, Input, Mismatch)), |
| 896 | } |
| 897 | } |
| 898 | |
| 899 | /// A non-negative integer no larger than 2^53 - 1, the largest a JSON |
| 900 | /// number carries exactly. |
| 901 | fn uint(&self, key: &str) -> Outcome<u64> { |
| 902 | let d = res!(self.must(key)); |
| 903 | let n = match d { |
| 904 | Dat::U8(n) => *n as u64, |
| 905 | Dat::U16(n) => *n as u64, |
| 906 | Dat::U32(n) => *n as u64, |
| 907 | Dat::U64(n) => *n, |
| 908 | Dat::I8(n) if *n >= 0 => *n as u64, |
| 909 | Dat::I16(n) if *n >= 0 => *n as u64, |
| 910 | Dat::I32(n) if *n >= 0 => *n as u64, |
| 911 | Dat::I64(n) if *n >= 0 => *n as u64, |
| 912 | other => return Err(err!( |
| 913 | "The '{}' of a {} is a non-negative integer, not {:?}.", key, self.what, other; |
| 914 | Invalid, Input, Mismatch)), |
| 915 | }; |
| 916 | if n > MAX_INT { |
| 917 | return Err(err!( |
| 918 | "The '{}' of a {} is {}, beyond 2^53 - 1, the largest integer JSON carries \ |
| 919 | exactly.", key, self.what, n; |
| 920 | Invalid, Input, TooBig)); |
| 921 | } |
| 922 | Ok(n) |
| 923 | } |
| 924 | |
| 925 | /// Unpadded base64url of exactly `N` bytes. |
| 926 | fn bytes<const N: usize>(&self, key: &str) -> Outcome<[u8; N]> { |
| 927 | fixed::<N>(&res!(self.text(key)), key, self.what) |
| 928 | } |
| 929 | |
| 930 | fn bytes_or_null<const N: usize>(&self, key: &str) -> Outcome<Option<[u8; N]>> { |
| 931 | match res!(self.must(key)) { |
| 932 | Dat::Empty => Ok(None), |
| 933 | Dat::Opt(inner) if inner.is_none() => Ok(None), |
| 934 | Dat::Str(s) => Ok(Some(res!(fixed::<N>(s, key, self.what)))), |
| 935 | other => Err(err!( |
| 936 | "The '{}' of a {} is base64url or null, not a {:?}.", key, self.what, other.kind(); |
| 937 | Invalid, Input, Mismatch)), |
| 938 | } |
| 939 | } |
| 940 | |
| 941 | fn words(&self, key: &str) -> Outcome<Vec<String>> { |
| 942 | let list = match res!(self.must(key)) { |
| 943 | Dat::List(list) => list, |
| 944 | other => return Err(err!( |
| 945 | "The '{}' of a {} is a list of words, not a {:?}.", key, self.what, other.kind(); |
| 946 | Invalid, Input, Mismatch)), |
| 947 | }; |
| 948 | let mut words = Vec::with_capacity(list.len()); |
| 949 | for d in list { |
| 950 | match d { |
| 951 | Dat::Str(s) => words.push(s.clone()), |
| 952 | other => return Err(err!( |
| 953 | "The '{}' of a {} holds a {:?} where a word belongs.", key, self.what, |
| 954 | other.kind(); |
| 955 | Invalid, Input, Mismatch)), |
| 956 | } |
| 957 | } |
| 958 | res!(check_words(&words, self.what)); |
| 959 | Ok(words) |
| 960 | } |
| 961 | } |
| 962 | |
| 963 | fn fixed<const N: usize>(b64: &str, key: &str, what: &str) -> Outcome<[u8; N]> { |
| 964 | let v = res!(base64::decode_url(b64)); |
| 965 | if v.len() != N { |
| 966 | return Err(err!( |
| 967 | "The '{}' of a {} decodes to {} bytes, not {}.", key, what, v.len(), N; |
| 968 | Invalid, Input, Size)); |
| 969 | } |
| 970 | let mut out = [0u8; N]; |
| 971 | out.copy_from_slice(&v); |
| 972 | Ok(out) |
| 973 | } |