1use std::fmt::Write as _;
23use std::path::Path;
24use std::sync::{Arc, Mutex};
25
26use base64::Engine as _;
27use base64::engine::general_purpose::URL_SAFE_NO_PAD;
28use clap::{Args, ValueEnum};
29use mkit_attest::eth;
30use mkit_attest::grant::{
31 Namespace, OwnerScheme, RelyingParty, RepositoryIdentity, SignedHeader, WebAuthnAssertion,
32 webauthn_challenge,
33};
34use mkit_core::hash::{hash, to_hex_bytes};
35use mkit_keystore::{Algorithm, KeyRef, KeySelector, KeySigner, open_backend};
36use mkit_transport_connect::EnvelopeSigner;
37
38use super::{HeaderError, verify_epoch_header, verify_grant_header, verify_visibility_header};
39use crate::config::Config;
40
41const MAX_IMPORT_BYTES: u64 = 64 * 1024;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
45pub enum SchemeArg {
46 #[value(name = "ed25519")]
47 Ed25519,
48 #[value(name = "secp256k1-eip191")]
49 Secp256k1Eip191,
50 #[value(name = "webauthn-p256")]
51 WebAuthnP256,
52}
53
54impl From<SchemeArg> for OwnerScheme {
55 fn from(arg: SchemeArg) -> Self {
56 match arg {
57 SchemeArg::Ed25519 => Self::Ed25519,
58 SchemeArg::Secp256k1Eip191 => Self::Secp256k1Eip191,
59 SchemeArg::WebAuthnP256 => Self::WebAuthnP256,
60 }
61 }
62}
63
64#[derive(Debug, Clone, Default, Args)]
66pub struct OwnerArgs {
67 #[arg(long, value_enum, value_name = "SCHEME")]
71 pub scheme: Option<SchemeArg>,
72 #[arg(long)]
75 pub print_statement: bool,
76 #[arg(long, value_name = "FILE")]
79 pub statement_file: Option<std::path::PathBuf>,
80 #[arg(long, value_name = "HEX", requires = "statement_file")]
83 pub signature: Option<String>,
84 #[arg(
88 long,
89 value_name = "FILE",
90 requires = "statement_file",
91 conflicts_with = "signature"
92 )]
93 pub webauthn_assertion: Option<std::path::PathBuf>,
94}
95
96pub struct NativeOwner {
98 namespace: Namespace,
99 scheme: OwnerScheme,
100 inner: NativeKey,
101}
102
103enum NativeKey {
104 Ed25519(Arc<dyn EnvelopeSigner>),
105 Secp256k1(Mutex<Box<dyn KeySigner>>),
106}
107
108impl std::fmt::Debug for NativeOwner {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("NativeOwner")
111 .field("namespace", &self.namespace.to_string())
112 .field("scheme", &self.scheme)
113 .finish_non_exhaustive()
114 }
115}
116
117impl NativeOwner {
118 pub fn ed25519(signer: Arc<dyn EnvelopeSigner>) -> Result<Self, String> {
124 let key = mkit_core::hash::from_hex(&signer.public_key_hex())
125 .map_err(|e| format!("signing key is not an Ed25519 public key: {e}"))?;
126 Ok(Self {
127 namespace: Namespace::Ed25519(key),
128 scheme: OwnerScheme::Ed25519,
129 inner: NativeKey::Ed25519(signer),
130 })
131 }
132
133 pub fn secp256k1(signer: Box<dyn KeySigner>) -> Result<Self, String> {
138 if signer.algorithm() != Algorithm::Secp256k1 {
139 return Err("the configured key is not a secp256k1 key".to_owned());
140 }
141 let public = signer
142 .public_key()
143 .map_err(|e| format!("keystore public key: {e}"))?;
144 let point = k256::ecdsa::VerifyingKey::from_sec1_bytes(public.as_bytes())
145 .map_err(|e| format!("keystore secp256k1 public key: {e}"))?
146 .to_sec1_point(false);
147 let xy: [u8; 64] = point
148 .as_bytes()
149 .get(1..65)
150 .and_then(|b| b.try_into().ok())
151 .ok_or("keystore secp256k1 public key has an unexpected encoding")?;
152 let address = eth::address_secp256k1(&xy).map_err(|e| e.to_string())?;
153 Ok(Self {
154 namespace: Namespace::Address(address),
155 scheme: OwnerScheme::Secp256k1Eip191,
156 inner: NativeKey::Secp256k1(Mutex::new(signer)),
157 })
158 }
159
160 pub fn open_secp256k1(cfg: &Config) -> Result<Self, String> {
165 let text = cfg.key.secp256k1_ref_or_fallback();
166 let key_ref = text
167 .parse::<KeyRef>()
168 .map_err(|e| format!("key.secp256k1_ref `{text}`: {e}"))?;
169 let store =
170 open_backend(key_ref.backend()).map_err(|e| format!("keystore backend: {e}"))?;
171 let selector = KeySelector::new(key_ref.label().to_owned(), Some(Algorithm::Secp256k1))
172 .map_err(|e| format!("key.secp256k1_ref `{text}`: {e}"))?;
173 let opener = store.opener().ok_or_else(|| {
174 format!(
175 "keystore backend `{}` does not support opening keys",
176 key_ref.backend()
177 )
178 })?;
179 let signer = opener.open(&selector).map_err(|e| {
180 format!(
181 "no secp256k1 key `{text}` — run `mkit key generate --backend {} --algorithm secp256k1 --label {}` first: {e}",
182 key_ref.backend(),
183 key_ref.label()
184 )
185 })?;
186 Self::secp256k1(signer)
187 }
188
189 #[must_use]
190 pub fn namespace(&self) -> &Namespace {
191 &self.namespace
192 }
193
194 #[must_use]
195 pub fn scheme(&self) -> OwnerScheme {
196 self.scheme
197 }
198
199 pub fn sign(&self, statement: &[u8]) -> Result<Vec<u8>, String> {
204 match &self.inner {
205 NativeKey::Ed25519(signer) => {
206 let hex = signer.sign_hex(&hash(statement))?;
207 hex::decode(hex).map_err(|e| format!("signer returned invalid hex: {e}"))
208 }
209 NativeKey::Secp256k1(signer) => {
210 let mut guard = signer
211 .lock()
212 .map_err(|_| "keystore signer mutex poisoned".to_owned())?;
213 let sig = guard
214 .sign_prehash_recoverable_secp256k1(ð::eip191_hash(statement))
215 .map_err(|e| match e {
216 mkit_keystore::Error::UnsupportedOperation(_) => format!(
217 "this key can't sign secp256k1-eip191 natively ({e}); only software-keystore secp256k1 keys can — use --print-statement and import a wallet signature instead"
218 ),
219 other => format!("keystore signing failed: {other}"),
220 })?;
221 Ok(sig.to_vec())
222 }
223 }
224 }
225}
226
227#[derive(Debug)]
229pub enum Plan {
230 Native(NativeOwner),
232 Print { namespace: Namespace },
234 Import {
236 statement: Vec<u8>,
237 scheme: OwnerScheme,
238 blob: Vec<u8>,
239 },
240}
241
242#[allow(missing_debug_implementations)] pub struct SignCtx<'a> {
245 pub ed25519: &'a dyn Fn() -> Result<NativeOwner, String>,
247 pub cfg: &'a Config,
248 pub relying_parties: &'a [RelyingParty],
249}
250
251pub fn resolve(
257 args: &OwnerArgs,
258 namespace_hint: Option<Namespace>,
259 ctx: &SignCtx<'_>,
260) -> Result<Plan, String> {
261 if let Some(path) = &args.statement_file {
262 if args.print_statement {
263 return Err("--print-statement and --statement-file can't be combined".to_owned());
264 }
265 let statement = read_statement_file(path)?;
266 let (scheme, blob) = match (&args.signature, &args.webauthn_assertion) {
267 (Some(hex), None) => (OwnerScheme::Secp256k1Eip191, import_eip191(hex)?.to_vec()),
268 (None, Some(file)) => (
269 OwnerScheme::WebAuthnP256,
270 import_webauthn(file, ctx.relying_parties)?,
271 ),
272 _ => {
273 return Err(
274 "--statement-file needs --signature <hex> or --webauthn-assertion <file>"
275 .to_owned(),
276 );
277 }
278 };
279 if let Some(chosen) = args.scheme
280 && OwnerScheme::from(chosen) != scheme
281 {
282 return Err(format!(
283 "--scheme {} doesn't match the imported signature ({})",
284 OwnerScheme::from(chosen).token(),
285 scheme.token()
286 ));
287 }
288 return Ok(Plan::Import {
289 statement,
290 scheme,
291 blob,
292 });
293 }
294 if args.print_statement {
295 let namespace = match namespace_hint {
296 Some(ns) => ns,
297 None => *native(args, ctx)?.namespace(),
298 };
299 return Ok(Plan::Print { namespace });
300 }
301 let owner = native(args, ctx)?;
302 if let Some(hint) = namespace_hint
303 && hint != *owner.namespace()
304 {
305 return Err(format!(
306 "the signing key owns namespace {}, not {hint}; to sign for {hint} with a wallet or authenticator use --print-statement",
307 owner.namespace()
308 ));
309 }
310 Ok(Plan::Native(owner))
311}
312
313fn native(args: &OwnerArgs, ctx: &SignCtx<'_>) -> Result<NativeOwner, String> {
314 match args.scheme.map_or(OwnerScheme::Ed25519, OwnerScheme::from) {
315 OwnerScheme::Ed25519 => (ctx.ed25519)(),
316 OwnerScheme::Secp256k1Eip191 => NativeOwner::open_secp256k1(ctx.cfg),
317 _ => Err(
318 "webauthn-p256 signatures come from an authenticator: run with --print-statement, sign the printed challenge, then pass --statement-file and --webauthn-assertion"
319 .to_owned(),
320 ),
321 }
322}
323
324fn read_bounded(path: &Path, what: &str) -> Result<Vec<u8>, String> {
325 std::fs::File::open(path)
326 .and_then(|file| super::read_bounded(file, MAX_IMPORT_BYTES))
327 .map_err(|e| format!("{what} {}: {e}", path.display()))
328}
329
330fn read_statement_file(path: &Path) -> Result<Vec<u8>, String> {
331 let bytes = read_bounded(path, "statement file")?;
332 if bytes.last() == Some(&b'\n') {
333 return Err(format!(
334 "statement file {} ends with a line feed, but a statement has none; write it with `--print-statement > {}` and don't edit it",
335 path.display(),
336 path.display()
337 ));
338 }
339 Ok(bytes)
340}
341
342pub fn import_eip191(hex_text: &str) -> Result<[u8; 65], String> {
348 let text = hex_text.trim();
349 let text = text
350 .strip_prefix("0x")
351 .or_else(|| text.strip_prefix("0X"))
352 .unwrap_or(text);
353 let bytes = hex::decode(text).map_err(|e| format!("--signature is not hex: {e}"))?;
354 let sig: [u8; 65] = bytes
355 .as_slice()
356 .try_into()
357 .map_err(|_| format!("--signature must be 65 bytes (r‖s‖v), got {}", bytes.len()))?;
358 eth::normalize_eip191_signature(sig).map_err(|e| format!("--signature: {e}"))
359}
360
361pub fn import_webauthn(path: &Path, rps: &[RelyingParty]) -> Result<Vec<u8>, String> {
369 if rps.is_empty() {
370 return Err(HeaderError::WebAuthnNotPinned.to_string());
371 }
372 let bytes = read_bounded(path, "WebAuthn assertion")?;
373 let json: serde_json::Value =
374 serde_json::from_slice(&bytes).map_err(|e| format!("WebAuthn assertion: not JSON: {e}"))?;
375 let field = |name: &str| -> Result<Vec<u8>, String> {
376 let text = json
377 .get(name)
378 .and_then(serde_json::Value::as_str)
379 .ok_or_else(|| format!("WebAuthn assertion: missing string field `{name}`"))?;
380 URL_SAFE_NO_PAD
381 .decode(text.trim_end_matches('='))
382 .map_err(|e| format!("WebAuthn assertion: `{name}` is not base64url: {e}"))
383 };
384 let public_key: [u8; 64] = field("publicKey")?
385 .as_slice()
386 .try_into()
387 .map_err(|_| "WebAuthn assertion: `publicKey` must be 64 bytes (x‖y)".to_owned())?;
388 let signature = field("signature")?;
389 let raw: [u8; 64] = match <[u8; 64]>::try_from(signature.as_slice()) {
390 Ok(raw) => {
391 eth::p256_check_raw_low_s(&raw).map_err(|e| {
392 format!("WebAuthn assertion: raw `signature` must be low-s ({e}); supply the DER form and mkit will normalize it")
393 })?;
394 raw
395 }
396 Err(_) => eth::p256_der_to_low_s_raw(&signature)
397 .map_err(|e| format!("WebAuthn assertion: `signature` is not strict DER ({e})"))?,
398 };
399 WebAuthnAssertion {
400 public_key,
401 authenticator_data: field("authenticatorData")?,
402 client_data_json: field("clientDataJSON")?,
403 signature: raw,
404 }
405 .encode()
406 .map_err(|e| format!("WebAuthn assertion: {e}"))
407}
408
409#[derive(Debug, Clone, Copy)]
411pub enum Kind<'a> {
412 Grant,
413 Epoch,
414 Visibility(&'a RepositoryIdentity),
415}
416
417#[derive(Debug, Clone)]
419pub struct Signed {
420 pub statement: Vec<u8>,
421 pub header: String,
422 pub scheme: OwnerScheme,
423}
424
425#[derive(Debug)]
427pub enum Produced {
428 Signed(Signed),
429 Print {
430 statement: Vec<u8>,
431 namespace: Namespace,
432 },
433}
434
435pub fn produce(
443 plan: Plan,
444 build: impl FnOnce(&Namespace) -> Result<Vec<u8>, String>,
445 kind: Kind<'_>,
446 rps: &[RelyingParty],
447 now_ms: i64,
448) -> Result<Produced, String> {
449 let (statement, scheme, blob) = match plan {
450 Plan::Print { namespace } => {
451 let statement = build(&namespace)?;
452 return Ok(Produced::Print {
453 statement,
454 namespace,
455 });
456 }
457 Plan::Native(owner) => {
458 let statement = build(owner.namespace())?;
459 let blob = owner.sign(&statement)?;
460 (statement, owner.scheme(), blob)
461 }
462 Plan::Import {
463 statement,
464 scheme,
465 blob,
466 } => (statement, scheme, blob),
467 };
468 let header = SignedHeader {
469 statement: statement.clone(),
470 scheme,
471 blob,
472 }
473 .encode()
474 .map_err(|e| format!("header: {e}"))?;
475 let rejected = |e: HeaderError| format!("the owner signature was rejected: {e}");
476 match kind {
477 Kind::Grant => verify_grant_header(&header, rps).map(|_| ()),
478 Kind::Epoch => verify_epoch_header(&header, rps, now_ms).map(|_| ()),
479 Kind::Visibility(repository) => {
480 verify_visibility_header(&header, repository, rps, now_ms).map(|_| ())
481 }
482 }
483 .map_err(rejected)?;
484 Ok(Produced::Signed(Signed {
485 statement,
486 header,
487 scheme,
488 }))
489}
490
491#[must_use]
493pub fn signing_instructions(statement: &[u8], namespace: &Namespace) -> String {
494 let mut out = format!(
495 "statement bytes: {} (namespace {namespace})\n",
496 statement.len()
497 );
498 match namespace {
499 Namespace::Ed25519(_) => {
500 let _ = writeln!(
501 out,
502 "ed25519 message (BLAKE3 of the statement): {}",
503 to_hex_bytes(&hash(statement))
504 );
505 }
506 Namespace::Address(_) => {
507 let _ = writeln!(
508 out,
509 "secp256k1-eip191: sign the statement bytes as an EIP-191 personal message (digest 0x{})",
510 to_hex_bytes(ð::eip191_hash(statement))
511 );
512 let _ = writeln!(
513 out,
514 "webauthn-p256: use this WebAuthn challenge (base64url): {}",
515 webauthn_challenge(statement)
516 );
517 }
518 }
519 out
520}
521
522#[cfg(test)]
523mod tests {
524 use k256::ecdsa::SigningKey as K256Key;
525 use mkit_attest::grant::{Capabilities, Grant, RepoScope};
526
527 use super::*;
528
529 struct DalekSigner(ed25519_dalek::SigningKey);
530 impl EnvelopeSigner for DalekSigner {
531 fn public_key_hex(&self) -> String {
532 to_hex_bytes(&self.0.verifying_key().to_bytes())
533 }
534 fn sign_hex(&self, message: &[u8; 32]) -> Result<String, String> {
535 use ed25519_dalek::Signer as _;
536 Ok(to_hex_bytes(&self.0.sign(message).to_bytes()))
537 }
538 }
539
540 struct K256KeySigner {
544 key: K256Key,
545 label: mkit_keystore::KeyLabel,
546 }
547 impl KeySigner for K256KeySigner {
548 fn algorithm(&self) -> Algorithm {
549 Algorithm::Secp256k1
550 }
551 fn label(&self) -> &mkit_keystore::KeyLabel {
552 &self.label
553 }
554 fn metadata(&self) -> mkit_keystore::Result<mkit_keystore::KeyMetadata> {
555 unreachable!()
556 }
557 fn public_key(&self) -> mkit_keystore::Result<mkit_keystore::PublicKeyBytes> {
558 Ok(mkit_keystore::PublicKeyBytes::new(
559 self.key
560 .verifying_key()
561 .to_sec1_point(true)
562 .as_bytes()
563 .to_vec(),
564 ))
565 }
566 fn keyid(&self) -> mkit_keystore::Result<mkit_keystore::KeyId> {
567 unreachable!()
568 }
569 fn sign(&mut self, _msg: &[u8]) -> mkit_keystore::Result<Vec<u8>> {
570 unreachable!()
571 }
572 fn sign_prehash_recoverable_secp256k1(
573 &mut self,
574 prehash: &[u8; 32],
575 ) -> mkit_keystore::Result<[u8; 65]> {
576 let (sig, recid) = self.key.sign_prehash_recoverable(prehash);
577 let mut out = [0u8; 65];
578 out[..64].copy_from_slice(&sig.normalize_s().to_bytes());
579 out[64] = 27 + recid.to_byte();
580 if sig.normalize_s() != sig {
581 out[64] = if out[64] == 27 { 28 } else { 27 };
582 }
583 Ok(out)
584 }
585 }
586
587 fn k256_owner(seed: u8) -> NativeOwner {
588 NativeOwner::secp256k1(Box::new(K256KeySigner {
589 key: K256Key::from_slice(&[seed; 32]).unwrap(),
590 label: mkit_keystore::KeyLabel::new("test").unwrap(),
591 }))
592 .unwrap()
593 }
594
595 fn ed_owner(seed: u8) -> NativeOwner {
596 NativeOwner::ed25519(Arc::new(DalekSigner(
597 ed25519_dalek::SigningKey::from_bytes(&[seed; 32]),
598 )))
599 .unwrap()
600 }
601
602 const NOW: i64 = 1_800_000_000_000;
603
604 fn grant_for(ns: &Namespace) -> Vec<u8> {
605 Grant {
606 namespace: *ns,
607 scope: RepoScope::Namespace,
608 grantee: [7; 32],
609 capabilities: Capabilities::Read,
610 audiences: vec!["https://git.example.com".to_owned()],
611 ref_scopes: None,
612 epoch: 0,
613 created_ms: NOW,
614 expiry_ms: NOW + 60_000,
615 nonce: [9; 32],
616 }
617 .encode()
618 .unwrap()
619 }
620
621 #[test]
622 fn native_ed25519_and_secp256k1_grants_verify() {
623 for owner in [ed_owner(3), k256_owner(0x11)] {
624 let scheme = owner.scheme();
625 let Produced::Signed(signed) = produce(
626 Plan::Native(owner),
627 |ns| Ok(grant_for(ns)),
628 Kind::Grant,
629 &[],
630 NOW,
631 )
632 .unwrap() else {
633 panic!("expected a signed header")
634 };
635 assert_eq!(signed.scheme, scheme);
636 assert!(verify_grant_header(&signed.header, &[]).is_ok());
637 }
638 }
639
640 #[test]
641 fn wallet_signatures_are_normalized_for_every_v_spelling_and_high_s() {
642 let owner = k256_owner(0x22);
643 let ns = *owner.namespace();
644 let statement = grant_for(&ns);
645 let good: [u8; 65] = owner.sign(&statement).unwrap().try_into().unwrap();
646 let n = hex::decode("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")
648 .unwrap();
649 let mut twin = good;
650 let mut borrow = 0i16;
651 for i in (0..32).rev() {
652 let d = i16::from(n[i]) - i16::from(good[32 + i]) - borrow;
653 borrow = i16::from(d < 0);
654 twin[32 + i] = (d + 256 * borrow).to_le_bytes()[0];
655 }
656 twin[64] = if good[64] == 27 { 28 } else { 27 };
657 for base in [good, twin] {
658 for v_style in [0u8, 27] {
659 let mut wallet = base;
660 wallet[64] = base[64] - 27 + v_style;
661 let imported = import_eip191(&hex::encode(wallet)).unwrap();
662 assert_eq!(imported, good, "v = {}", wallet[64]);
663 let Produced::Signed(signed) = produce(
664 Plan::Import {
665 statement: statement.clone(),
666 scheme: OwnerScheme::Secp256k1Eip191,
667 blob: imported.to_vec(),
668 },
669 |_| unreachable!("imports don't build"),
670 Kind::Grant,
671 &[],
672 NOW,
673 )
674 .unwrap() else {
675 panic!()
676 };
677 assert!(verify_grant_header(&signed.header, &[]).is_ok());
678 }
679 }
680 assert!(import_eip191(&format!("0x{}", hex::encode(good))).is_ok());
682 assert!(import_eip191(&format!("0X{}", hex::encode(good))).is_ok());
683 assert!(import_eip191(&hex::encode(&good[..64])).is_err());
684 let mut bad_v = good;
685 bad_v[64] = 5;
686 assert!(import_eip191(&hex::encode(bad_v)).is_err());
687 }
688
689 #[test]
690 fn wallet_vectors_from_the_golden_file_normalize() {
691 let golden: serde_json::Value = serde_json::from_str(include_str!(
692 "../../../../tests/golden/grants/eth-primitives.json"
693 ))
694 .unwrap();
695 for case in golden["high_s"].as_array().unwrap() {
696 let high = case["signature"].as_str().unwrap();
697 let low = case["normalized"].as_str().unwrap();
698 let high_bytes = hex::decode(high).unwrap();
699 for v_style in [0u8, 27] {
700 let mut sig = high_bytes.clone();
701 sig[64] = sig[64] - 27 + v_style;
702 assert_eq!(
703 hex::encode(import_eip191(&hex::encode(sig)).unwrap()),
704 low,
705 "{}",
706 case["name"]
707 );
708 }
709 }
710 }
711
712 #[test]
713 fn a_signature_from_another_key_is_rejected_naming_the_rule() {
714 let owner = k256_owner(0x22);
715 let statement = grant_for(owner.namespace());
716 let other = k256_owner(0x33).sign(&statement).unwrap();
717 let error = produce(
718 Plan::Import {
719 statement,
720 scheme: OwnerScheme::Secp256k1Eip191,
721 blob: other,
722 },
723 |_| unreachable!(),
724 Kind::Grant,
725 &[],
726 NOW,
727 )
728 .unwrap_err();
729 assert!(error.contains("owner mismatch"), "{error}");
730 }
731
732 #[test]
733 fn webauthn_import_is_refused_without_a_pinned_relying_party() {
734 let dir = tempfile::tempdir().unwrap();
735 let file = dir.path().join("assertion.json");
736 std::fs::write(&file, b"{}").unwrap();
737 let error = import_webauthn(&file, &[]).unwrap_err();
738 assert!(error.contains("pinned relying party"), "{error}");
739 }
740
741 #[test]
742 fn webauthn_der_signatures_are_normalized_to_low_s() {
743 let golden: serde_json::Value = serde_json::from_str(include_str!(
744 "../../../../tests/golden/grants/eth-primitives.json"
745 ))
746 .unwrap();
747 let case = golden["p256_der"]
748 .as_array()
749 .unwrap()
750 .iter()
751 .find(|c| c["name"] == "high-s")
752 .unwrap();
753 let der = hex::decode(case["der"].as_str().unwrap()).unwrap();
754 let low = hex::decode(case["raw_low_s"].as_str().unwrap()).unwrap();
755 let b64 = |b: &[u8]| URL_SAFE_NO_PAD.encode(b);
756 let dir = tempfile::tempdir().unwrap();
757 let file = dir.path().join("assertion.json");
758 std::fs::write(
759 &file,
760 serde_json::json!({
761 "publicKey": b64(&[1u8; 64]),
762 "authenticatorData": b64(&[0u8; 37]),
763 "clientDataJSON": b64(b"{}"),
764 "signature": b64(&der),
765 })
766 .to_string(),
767 )
768 .unwrap();
769 let rps = vec![RelyingParty::new("example.com", ["https://example.com"]).unwrap()];
770 let blob = import_webauthn(&file, &rps).unwrap();
771 let parsed = WebAuthnAssertion::parse(&blob).unwrap();
772 assert_eq!(parsed.signature.as_slice(), low.as_slice());
773 let raw_high = hex::decode(case["raw_high_s"].as_str().unwrap()).unwrap();
775 std::fs::write(
776 &file,
777 serde_json::json!({
778 "publicKey": b64(&[1u8; 64]),
779 "authenticatorData": b64(&[0u8; 37]),
780 "clientDataJSON": b64(b"{}"),
781 "signature": b64(&raw_high),
782 })
783 .to_string(),
784 )
785 .unwrap();
786 assert!(import_webauthn(&file, &rps).unwrap_err().contains("low-s"));
787 }
788
789 #[test]
790 fn print_plan_returns_the_statement_and_instructions_name_the_digests() {
791 let owner = k256_owner(0x44);
792 let ns = *owner.namespace();
793 let Produced::Print {
794 statement,
795 namespace,
796 } = produce(
797 Plan::Print { namespace: ns },
798 |ns| Ok(grant_for(ns)),
799 Kind::Grant,
800 &[],
801 NOW,
802 )
803 .unwrap()
804 else {
805 panic!()
806 };
807 assert_eq!(namespace, ns);
808 let text = signing_instructions(&statement, &namespace);
809 assert!(text.contains(&to_hex_bytes(ð::eip191_hash(&statement))));
810 assert!(text.contains(&webauthn_challenge(&statement)));
811 }
812
813 #[test]
814 fn namespace_hint_must_match_the_native_key() {
815 let owner = ed_owner(5);
816 let cfg = Config::with_defaults();
817 let make = || Ok(ed_owner(5));
818 let ctx = SignCtx {
819 ed25519: &make,
820 cfg: &cfg,
821 relying_parties: &[],
822 };
823 let args = OwnerArgs {
824 scheme: None,
825 print_statement: false,
826 statement_file: None,
827 signature: None,
828 webauthn_assertion: None,
829 };
830 let other = Namespace::Address([1; 20]);
831 assert!(
832 resolve(&args, Some(other), &ctx)
833 .unwrap_err()
834 .contains("--print-statement")
835 );
836 assert!(resolve(&args, Some(*owner.namespace()), &ctx).is_ok());
837 }
838
839 #[test]
840 fn statement_files_with_a_trailing_line_feed_are_refused() {
841 let dir = tempfile::tempdir().unwrap();
842 let file = dir.path().join("s.txt");
843 std::fs::write(&file, b"x\n").unwrap();
844 assert!(
845 read_statement_file(&file)
846 .unwrap_err()
847 .contains("line feed")
848 );
849 }
850}