1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// The same cases as rust/host/src/serviceauth.rs.
import assert from "node:assert/strict";
import { test } from "node:test";
import { base58 } from "../encoding/index.js";
import { createDidKey } from "../didkey/index.js";
import { secp256k1 } from "../server/index.js";
import { cachedResolve, MAX_TTL_S, replayCache, SKEW_S, verifyServiceAuth } from "./index.js";
const ISS = "did:plc:caller0000000000000000000";
const AUD = "did:key:zDnaeService";
const LXM = "com.example.launch";
const NOW = 1_800_000_000;
const auth = { audiences: [AUD, "did:web:svc.example"], issuers: [ISS] };
const KEY = new Uint8Array(32).fill(7);
const enc = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
const jwt = (alg, claims, sign) => {
const signing = `${enc({ typ: "JWT", alg })}.${enc(claims)}`;
return `${signing}.${Buffer.from(sign(Buffer.from(signing))).toString("base64url")}`;
};
const claims = (extra = {}) => ({ iss: ISS, aud: AUD, lxm: LXM, iat: NOW, exp: NOW + 60, jti: "x", ...extra });
const es256k = (c, key = KEY) => jwt("ES256K", c, (m) => secp256k1.sign(m, key));
const docFor = (key) => ({ verificationMethod: [{ id: "#atproto", publicKeyMultibase: `z${base58([0xe7, 0x01, ...secp256k1.getPublicKey(key, true)])}` }] });
const resolve = async () => docFor(KEY);
const check = (header, extra = {}) => verifyServiceAuth(auth, header, LXM, { now: NOW, resolve, replay: replayCache(), ...extra });
test("an ES256K token from an allowed issuer passes; aud may carry a #fragment", async () => {
assert.equal(await check(`Bearer ${es256k(claims())}`), ISS);
assert.equal(await check(`Bearer ${es256k(claims({ aud: "did:web:svc.example#svc" }))}`), ISS);
assert.equal(await check(`Bearer ${es256k(claims({ iat: NOW + SKEW_S }))}`), ISS, "iat within the skew");
});
test("an ES256 token (a did:key's service auth) passes", async () => {
const key = await createDidKey();
const token = await key.serviceAuth({ aud: AUD, lxm: LXM });
const doc = { verificationMethod: [{ id: "did:key:x#atproto", publicKeyMultibase: key.did.slice("did:key:".length) }] };
assert.equal(await verifyServiceAuth({ audiences: [AUD], issuers: [key.did] }, `Bearer ${token}`, LXM, { resolve: async () => doc }), key.did);
});
test("bad tokens are refused", async () => {
const refused = async (header, pattern) => assert.rejects(check(header), pattern);
await refused(undefined, /Bearer/);
await refused("Basic x", /Bearer/);
await refused("Bearer a.b", /not a JWT/);
await refused(`Bearer ${es256k(claims({ lxm: "other.method" }))}`, /not com\.example\.launch/);
await refused(`Bearer ${es256k(claims({ aud: "did:web:someone.else" }))}`, /is not this service/);
await refused(`Bearer ${es256k(claims({ exp: NOW }))}`, /expired/);
await refused(`Bearer ${es256k(claims({ exp: NOW + MAX_TTL_S + 1 }))}`, /more than 3600 s away/);
await refused(`Bearer ${es256k(claims({ iat: undefined }))}`, /iat/);
await refused(`Bearer ${es256k(claims({ iat: NOW + SKEW_S + 1 }))}`, /iat/);
await refused(`Bearer ${es256k(claims({ iss: "did:plc:stranger" }))}`, /not allowed/);
await refused(`Bearer ${es256k(claims(), new Uint8Array(32).fill(9))}`, /bad signature/);
// a valid signature with S flipped high (atproto requires low S)
const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const high = jwt("ES256K", claims(), (m) => {
const rs = secp256k1.sign(m, KEY);
const s = BigInt(`0x${Buffer.from(rs.subarray(32)).toString("hex")}`);
rs.set(Buffer.from((N - s).toString(16).padStart(64, "0"), "hex"), 32);
return rs;
});
await refused(`Bearer ${high}`, /high S/);
const wrong = es256k(claims()).replace(enc({ typ: "JWT", alg: "ES256K" }), enc({ typ: "JWT", alg: "ES256" }));
await refused(`Bearer ${wrong}`, /does not match the issuer/);
});
test("a token is good once until it expires; a token without jti is keyed by its hash", async () => {
const replay = replayCache();
const header = `Bearer ${es256k(claims())}`;
assert.equal(await check(header, { replay }), ISS);
await assert.rejects(check(header, { replay, now: NOW + 1 }), /the token was already used/);
await assert.rejects(check(`Bearer ${es256k(claims({ exp: NOW + 30 }))}`, { replay }), /already used/, "same iss + jti");
assert.equal(await check(`Bearer ${es256k(claims({ jti: "y" }))}`, { replay }), ISS);
const bare = `Bearer ${es256k(claims({ jti: undefined }))}`;
assert.equal(await check(bare, { replay }), ISS);
await assert.rejects(check(bare, { replay }), /already used/);
assert.equal(replay.seen.size, 3);
// The signature's spare trailing bits: the next base64url symbol decodes to the same 64 bytes, a
// second replay key for one signature unless only the canonical form is taken.
const B64U = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const flipped = bare.slice(0, -1) + B64U[B64U.indexOf(bare.at(-1)) + 1];
assert.deepEqual(Buffer.from(flipped.split(".")[2], "base64url"), Buffer.from(bare.split(".")[2], "base64url"));
await assert.rejects(check(flipped, { replay }), /signature is not base64url/);
});
test("the replay cache is bounded: expired entries go first; full of live tokens it refuses, never evicts", () => {
const replay = replayCache(2);
replay.admit("a", 60, 0);
replay.admit("x", 3600, 0);
assert.throws(() => replay.admit("y", 3600, 0), /busy: too many tokens in use/);
assert.throws(() => replay.admit("a", 60, 1), /already used/, "a live token stays remembered");
replay.admit("d", 3600, 61); // a has expired: its slot is free
assert.deepEqual([...replay.seen.keys()], ["x", "d"]);
replay.admit("a", 3700, 3601); // past its exp a key may come back
});
test("DID documents are cached for a while and bounded; failures are not kept", async () => {
let t = 0;
const fetched = [];
const resolve = cachedResolve(async (did) => {
fetched.push(did);
if (did === "did:plc:bad") throw new Error("nope");
return { id: did };
}, { ttlMs: 100, max: 2, clock: () => t });
for (const did of ["did:plc:a", "did:plc:a", "did:plc:b", "did:plc:a"]) assert.equal((await resolve(did)).id, did);
assert.deepEqual(fetched, ["did:plc:a", "did:plc:b"]);
await assert.rejects(resolve("did:plc:bad"));
await assert.rejects(resolve("did:plc:bad"));
await resolve("did:plc:c");
await resolve("did:plc:a");
assert.deepEqual(fetched.slice(2), ["did:plc:bad", "did:plc:bad", "did:plc:c", "did:plc:a"]);
t = 150;
await resolve("did:plc:c");
assert.equal(fetched.length, 7);
});