@yozz.app/tls 0.2.0 → 0.3.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.
- package/README.md +9 -2
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +4 -3
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ const result = await startTls({
|
|
|
25
25
|
transport,
|
|
26
26
|
serverName: 'imap.example.com',
|
|
27
27
|
trustAnchors: compileAnchors(ROOT_BUNDLE).source,
|
|
28
|
-
validationTime:
|
|
28
|
+
validationTime: Temporal.Now.instant(),
|
|
29
29
|
validator: YOZZ_VALIDATOR,
|
|
30
30
|
});
|
|
31
31
|
if (result.ok) {
|
|
@@ -35,7 +35,14 @@ if (result.ok) {
|
|
|
35
35
|
|
|
36
36
|
`HandshakeResult` is a discriminated union, never a throw for a refused certificate; the refusal
|
|
37
37
|
says which check failed. A session handed to `onSession` can be offered once on a later
|
|
38
|
-
connection to resume.
|
|
38
|
+
connection to resume. Its `receivedAt` and `authenticatedAt` are `Temporal.Instant`s, which neither
|
|
39
|
+
structured clone nor JSON keeps, so a store encodes them. Before 0.3 they were `Date`s: a store
|
|
40
|
+
written by 0.2 must convert them, or `startTls` throws on the session before a byte goes out.
|
|
41
|
+
|
|
42
|
+
**Requires a global `Temporal`.** Times are `Temporal.Instant`s (`validationTime`, the optional `now` clock, a session's
|
|
43
|
+
instants). Node 26, Chrome and Firefox ship it; where the runtime has none (Safari, Node 24), install
|
|
44
|
+
[`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill) and load it before this package is
|
|
45
|
+
imported (`import 'temporal-polyfill/global'`). The package imports no polyfill itself.
|
|
39
46
|
|
|
40
47
|
`src/index.ts` is the whole public API and says why each export exists. The source is in
|
|
41
48
|
[fishballapp/yozz](https://github.com/fishballapp/yozz), which takes issues but no pull requests.
|
package/dist/index.d.mts
CHANGED
|
@@ -107,14 +107,14 @@ type TlsSession = {
|
|
|
107
107
|
readonly preSharedKey: Uint8Array;
|
|
108
108
|
/** §4.3.11.1's `ticket_age_add`, the server's random offset. */
|
|
109
109
|
readonly ticketAgeAdd: number;
|
|
110
|
-
/**
|
|
111
|
-
readonly receivedAt:
|
|
110
|
+
/** Neither structured clone nor JSON keeps a `Temporal.Instant`: a store encodes it. */
|
|
111
|
+
readonly receivedAt: Temporal.Instant;
|
|
112
112
|
readonly lifetimeSeconds: number;
|
|
113
113
|
/**
|
|
114
114
|
* When the peer's certificate was last verified, carried forward unchanged through every renewal:
|
|
115
|
-
* RFC 9846 §4.7.1 asks for a limit on the total lifetime of renewed keying material.
|
|
115
|
+
* RFC 9846 §4.7.1 asks for a limit on the total lifetime of renewed keying material.
|
|
116
116
|
*/
|
|
117
|
-
readonly authenticatedAt:
|
|
117
|
+
readonly authenticatedAt: Temporal.Instant;
|
|
118
118
|
/** Carried forward through renewals. A name rather than a code point, so it survives the caller's store. */
|
|
119
119
|
readonly peerSignatureScheme: SignatureScheme;
|
|
120
120
|
/** Carried forward, so a resumed handshake can re-validate the chain against today's clock and anchors (`reverifyOnResume`). */
|
|
@@ -132,7 +132,7 @@ type StartTlsOptions = {
|
|
|
132
132
|
readonly transport: ByteDuplex;
|
|
133
133
|
readonly serverName: string;
|
|
134
134
|
readonly trustAnchors: TrustAnchorSource;
|
|
135
|
-
readonly validationTime:
|
|
135
|
+
readonly validationTime: Temporal.Instant;
|
|
136
136
|
readonly validator: Validator;
|
|
137
137
|
readonly expectedPeerName?: PeerName | null;
|
|
138
138
|
/** In preference order; the first carries the key share. */
|
|
@@ -154,7 +154,7 @@ type StartTlsOptions = {
|
|
|
154
154
|
/** Tickets arrive after the handshake, so this fires from `read()`. Without it, tickets are dropped. */
|
|
155
155
|
readonly onSession?: (session: TlsSession) => void | Promise<void>;
|
|
156
156
|
/** The running clock for ticket ages, distinct from `validationTime` (a policy input a caller may freeze). Injectable for BoGo. */
|
|
157
|
-
readonly now?: () =>
|
|
157
|
+
readonly now?: () => Temporal.Instant;
|
|
158
158
|
};
|
|
159
159
|
type HandshakeResult = {
|
|
160
160
|
readonly ok: true;
|
package/dist/index.mjs
CHANGED
|
@@ -1839,6 +1839,7 @@ const isDerChain = (chain) => chain?.leafDer instanceof Uint8Array && Array.isAr
|
|
|
1839
1839
|
const assertUsableSession = (session) => {
|
|
1840
1840
|
if (!SUPPORTED_SIGNATURE_SCHEMES.includes(session.peerSignatureScheme)) throw new Error("the stored session names no signature scheme this client implements");
|
|
1841
1841
|
if (!isDerChain(session.peerCertificateChain)) throw new Error("the stored session carries no usable peer certificate chain");
|
|
1842
|
+
for (const field of ["receivedAt", "authenticatedAt"]) if (!(session[field] instanceof Temporal.Instant)) throw new Error(`the stored session's ${field} is not a Temporal.Instant`);
|
|
1842
1843
|
};
|
|
1843
1844
|
/**
|
|
1844
1845
|
* A resumed handshake verifies no signature, so a ticket it mints inherits the original
|
|
@@ -1858,10 +1859,10 @@ const inheritedAuthentication = (resumed, verifiedNow) => {
|
|
|
1858
1859
|
return verifiedNow;
|
|
1859
1860
|
};
|
|
1860
1861
|
const isWithinAuthenticationCeiling = (authenticatedAt, now) => {
|
|
1861
|
-
const age = now.
|
|
1862
|
+
const age = now.epochMilliseconds - authenticatedAt.epochMilliseconds;
|
|
1862
1863
|
return age >= 0 && age < MAX_AUTHENTICATION_AGE_SECONDS * 1e3;
|
|
1863
1864
|
};
|
|
1864
|
-
const ticketAgeMs = (session, now) => now.
|
|
1865
|
+
const ticketAgeMs = (session, now) => now.epochMilliseconds - session.receivedAt.epochMilliseconds;
|
|
1865
1866
|
/** RFC 9846 §4.3.11.1: age in milliseconds plus the server's offset, modulo 2^32. */
|
|
1866
1867
|
const obfuscatedTicketAge = (session, now) => (ticketAgeMs(session, now) + session.ticketAgeAdd) % 4294967296;
|
|
1867
1868
|
/** `asciiLower`, not `toLowerCase`: KELVIN SIGN folds to `k` under Unicode, which would widen a security check. */
|
|
@@ -2231,7 +2232,7 @@ const clientHelloRecords = (message, legacyVersion) => Array.from({ length: Math
|
|
|
2231
2232
|
/** Exported for `replay.ts` only, which `index.test.ts` enforces. */
|
|
2232
2233
|
const runHandshake = async (options, replay) => {
|
|
2233
2234
|
const { transport, serverName } = options;
|
|
2234
|
-
const now = options.now ?? (() =>
|
|
2235
|
+
const now = options.now ?? (() => Temporal.Instant.fromEpochMilliseconds(Date.now()));
|
|
2235
2236
|
const expectedPeerName = options.expectedPeerName !== void 0 ? options.expectedPeerName : {
|
|
2236
2237
|
kind: "dns",
|
|
2237
2238
|
value: serverName
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yozz.app/tls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A TLS 1.3 client in TypeScript over any byte duplex: record layer, handshake, key schedule, PSK resumption, KeyUpdate, TOFU pinning. Runs in Node and browsers, no native code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/fishballapp/yozz",
|
|
@@ -29,11 +29,12 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@yozz.app/x509": "0.
|
|
32
|
+
"@yozz.app/x509": "0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@playwright/test": "1.
|
|
35
|
+
"@playwright/test": "1.64.1",
|
|
36
36
|
"@types/node": "^24.13.4",
|
|
37
|
+
"temporal-polyfill": "^1.0.8",
|
|
37
38
|
"tsdown": "^0.23.0",
|
|
38
39
|
"vite": "^8.3.0",
|
|
39
40
|
"wrangler": "^4.131.2"
|