provared 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +1062 -0
  2. package/LICENSE +202 -0
  3. package/LICENSE-CC-BY-4.0.txt +396 -0
  4. package/README.md +689 -0
  5. package/bin/provared-check.mjs +565 -0
  6. package/bin/provared-headers.mjs +110 -0
  7. package/docs/threat-model.md +997 -0
  8. package/net/headers.js +403 -0
  9. package/package.json +57 -0
  10. package/page/check-page.js +162 -0
  11. package/page/index.html +47 -0
  12. package/page/render.js +530 -0
  13. package/page/style.css +150 -0
  14. package/samples/office-supplies.expected.json +82 -0
  15. package/samples/office-supplies.jsonl +10 -0
  16. package/samples/office-supplies.show.json +59 -0
  17. package/spec/provared-format.md +1672 -0
  18. package/src/actions.js +90 -0
  19. package/src/approval.js +73 -0
  20. package/src/blockstamp.js +198 -0
  21. package/src/book.js +2232 -0
  22. package/src/check.d.ts +21 -0
  23. package/src/check.js +15 -0
  24. package/src/compare.js +276 -0
  25. package/src/cover.js +198 -0
  26. package/src/der.js +192 -0
  27. package/src/encoding.js +286 -0
  28. package/src/fields.js +94 -0
  29. package/src/guard.js +171 -0
  30. package/src/headers.js +380 -0
  31. package/src/index.d.ts +2 -0
  32. package/src/index.js +37 -0
  33. package/src/jws.js +235 -0
  34. package/src/keys.js +149 -0
  35. package/src/pass.js +67 -0
  36. package/src/passkey-browser.d.ts +20 -0
  37. package/src/passkey-browser.js +171 -0
  38. package/src/recorder.js +1115 -0
  39. package/src/seal.js +133 -0
  40. package/src/service.js +124 -0
  41. package/src/signatures.js +147 -0
  42. package/src/slip.js +401 -0
  43. package/src/standing.js +181 -0
  44. package/src/stub.js +140 -0
  45. package/src/timestamp.js +351 -0
  46. package/src/tools.js +192 -0
  47. package/src/tree.js +143 -0
  48. package/src/types.d.ts +628 -0
  49. package/src/webauthn.js +119 -0
@@ -0,0 +1,119 @@
1
+ // Checking a passkey signature (a Web Authentication assertion), later and
2
+ // offline. The steps are those of W3C Web Authentication Level 3, section
3
+ // 7.2 ("Verifying an Authentication Assertion"), as far as they apply
4
+ // without a server session: format description, section 4.4.
5
+
6
+ import { Refusal, concatBytes, equalBytes, fromUtf8, sha256, toBase64url, utf8 } from './encoding.js';
7
+ import { verifySignature } from './signatures.js';
8
+
9
+ // The flags byte of the authenticator data (Web Authentication, section 6.1).
10
+ const FLAG_USER_PRESENT = 0x01;
11
+ const FLAG_USER_VERIFIED = 0x04;
12
+ const FLAG_ATTESTED_DATA = 0x40;
13
+ const FLAG_EXTENSION_DATA = 0x80;
14
+
15
+ function badData(why) {
16
+ return new Refusal('passkey-bad-data', why);
17
+ }
18
+
19
+ /**
20
+ * A passkey returns an ES256 signature in ASN.1 DER form (Web
21
+ * Authentication, section 6.5.5). Web Crypto checks the two numbers side by
22
+ * side, 32 bytes each. This converts one into the other and refuses any DER
23
+ * that is not strictly encoded.
24
+ * @param {Uint8Array} der
25
+ * @returns {Uint8Array} 64 bytes
26
+ */
27
+ export function ecdsaDerToRaw(der) {
28
+ // SEQUENCE, short-form length (two 33-byte INTEGERs at most: 70 bytes).
29
+ if (der.length < 8 || der.length > 72 || der[0] !== 0x30 || der[1] !== der.length - 2) {
30
+ throw badData('The passkey signature is not a well-formed ECDSA signature.');
31
+ }
32
+ const out = new Uint8Array(64);
33
+ let at = 2;
34
+ for (let part = 0; part < 2; part++) {
35
+ if (at + 2 > der.length || der[at] !== 0x02) throw badData('The passkey signature is not a well-formed ECDSA signature.');
36
+ const length = der[at + 1];
37
+ const start = at + 2;
38
+ const end = start + length;
39
+ if (length < 1 || length > 33 || end > der.length) throw badData('The passkey signature is not a well-formed ECDSA signature.');
40
+ // A positive whole number in its shortest form.
41
+ if (der[start] & 0x80) throw badData('The passkey signature holds a negative number.');
42
+ if (length > 1 && der[start] === 0x00 && !(der[start + 1] & 0x80)) {
43
+ throw badData('The passkey signature is not in its shortest form.');
44
+ }
45
+ const digits = der[start] === 0x00 ? der.subarray(start + 1, end) : der.subarray(start, end);
46
+ if (digits.length > 32) throw badData('The passkey signature holds a number that is too large.');
47
+ out.set(digits, part * 32 + (32 - digits.length));
48
+ at = end;
49
+ }
50
+ if (at !== der.length) throw badData('The passkey signature has bytes left over.');
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Check a passkey signature over some bytes.
56
+ *
57
+ * @param {object} a
58
+ * @param {any} a.key the passkey's public key, already passed by checkKey
59
+ * @param {string} a.rpId the website the passkey belongs to
60
+ * @param {string} a.origin the address of the page it was used on
61
+ * @param {Uint8Array} a.signedBytes the bytes the signature must cover; the challenge is their SHA-256 hash
62
+ * @param {Uint8Array} a.authenticatorData as the passkey returned it
63
+ * @param {Uint8Array} a.clientDataJSON as the passkey returned it
64
+ * @param {Uint8Array} a.signature as the passkey returned it
65
+ * @param {string[]} [without] see verifySignature
66
+ * @returns {Promise<'valid'|'unavailable'>} "unavailable" when this device cannot check the passkey's signing method
67
+ * @throws {Refusal} with a code that says which check failed
68
+ */
69
+ export async function checkAssertion({ key, rpId, origin, signedBytes, authenticatorData, clientDataJSON, signature }, without = []) {
70
+ let client;
71
+ try {
72
+ client = JSON.parse(fromUtf8(clientDataJSON, 'passkey-bad-data'));
73
+ } catch {
74
+ throw badData('The passkey\'s client data is not JSON.');
75
+ }
76
+ if (client === null || typeof client !== 'object' || Array.isArray(client)) throw badData('The passkey\'s client data is not an object.');
77
+
78
+ // This must be an answer to "sign", not to "create a passkey".
79
+ if (client.type !== 'webauthn.get') {
80
+ throw new Refusal('passkey-wrong-type', 'The passkey answer was not made for signing.');
81
+ }
82
+ // The challenge is the hash of the bytes that had to be signed.
83
+ const challenge = toBase64url(await sha256(signedBytes));
84
+ if (client.challenge !== challenge) {
85
+ throw new Refusal('passkey-challenge-mismatch', 'The passkey signed something other than this slip.');
86
+ }
87
+ // The page it was used on.
88
+ if (client.origin !== origin || client.crossOrigin === true) {
89
+ throw new Refusal('passkey-origin-mismatch', 'The passkey was used on a page other than the one the slip names.');
90
+ }
91
+
92
+ // The authenticator data: which website, and the two flags.
93
+ if (authenticatorData.length < 37) throw badData('The passkey\'s authenticator data is cut short.');
94
+ const rpIdHash = await sha256(utf8(rpId));
95
+ if (!equalBytes(authenticatorData.subarray(0, 32), rpIdHash)) {
96
+ throw new Refusal('passkey-rpid-mismatch', 'The passkey belongs to a website other than the one the slip names.');
97
+ }
98
+ const flags = authenticatorData[32];
99
+ if (!(flags & FLAG_USER_PRESENT)) {
100
+ throw new Refusal('passkey-user-not-present', 'The passkey did not record that a person was present.');
101
+ }
102
+ if (!(flags & FLAG_USER_VERIFIED)) {
103
+ throw new Refusal('passkey-user-not-verified', 'The device did not confirm the person by fingerprint, face or PIN.');
104
+ }
105
+ // An answer to "sign" never carries a new passkey's data, and carries
106
+ // further bytes only when it says so.
107
+ if (flags & FLAG_ATTESTED_DATA) throw badData('The passkey\'s authenticator data holds data that belongs to creating a passkey.');
108
+ if (!(flags & FLAG_EXTENSION_DATA) && authenticatorData.length !== 37) {
109
+ throw badData('The passkey\'s authenticator data has bytes left over.');
110
+ }
111
+
112
+ // The signature covers the authenticator data followed by
113
+ // the hash of the client data.
114
+ const signed = concatBytes(authenticatorData, await sha256(clientDataJSON));
115
+ const raw = key.alg === 'ES256' ? ecdsaDerToRaw(signature) : signature;
116
+ const state = await verifySignature(key, raw, signed, without);
117
+ if (state === 'invalid') throw new Refusal('signature-invalid', 'The passkey signature does not fit the key the slip names.');
118
+ return state;
119
+ }