~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
115 lines · 6.3 KB Raw History
// 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);
});