| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Effect } from "effect" |
| 3 | import * as EventLogSessionAuth from "effect/eventlog/EventLogSessionAuth" |
| 4 | |
| 5 | const makeSigningKeyPair = Effect.promise(async () => { |
| 6 | const keyPair = await globalThis.crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]) |
| 7 | |
| 8 | if (!("privateKey" in keyPair) || !("publicKey" in keyPair)) { |
| 9 | throw new Error("Expected Ed25519 CryptoKeyPair") |
| 10 | } |
| 11 | |
| 12 | return { |
| 13 | signingPublicKey: new Uint8Array(await globalThis.crypto.subtle.exportKey("raw", keyPair.publicKey)), |
| 14 | signingPrivateKey: new Uint8Array(await globalThis.crypto.subtle.exportKey("pkcs8", keyPair.privateKey)) |
| 15 | } |
| 16 | }) |
| 17 | |
| 18 | const makePayload = (signingPublicKey: Uint8Array): EventLogSessionAuth.SessionAuthPayload => ({ |
| 19 | remoteId: "remote-123", |
| 20 | challenge: Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]), |
| 21 | publicKey: "client-public-key", |
| 22 | signingPublicKey |
| 23 | }) |
| 24 | |
| 25 | describe("EventLogSessionAuth", () => { |
| 26 | it.effect("encodes and decodes canonical auth payload deterministically", () => |
| 27 | Effect.gen(function*() { |
| 28 | const signingPublicKey = new Uint8Array(EventLogSessionAuth.Ed25519PublicKeyLength) |
| 29 | for (let i = 0; i < signingPublicKey.byteLength; i++) { |
| 30 | signingPublicKey[i] = i |
| 31 | } |
| 32 | |
| 33 | const payload = makePayload(signingPublicKey) |
| 34 | const encodedA = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload) |
| 35 | const encodedB = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload) |
| 36 | |
| 37 | assert.deepStrictEqual(encodedA, encodedB) |
| 38 | |
| 39 | const decoded = yield* EventLogSessionAuth.decodeSessionAuthPayload(encodedA) |
| 40 | assert.strictEqual(decoded.remoteId, payload.remoteId) |
| 41 | assert.strictEqual(decoded.publicKey, payload.publicKey) |
| 42 | assert.deepStrictEqual(decoded.challenge, payload.challenge) |
| 43 | assert.deepStrictEqual(decoded.signingPublicKey, payload.signingPublicKey) |
| 44 | })) |
| 45 | |
| 46 | it.effect("signs and verifies canonical auth payload with Ed25519", () => |
| 47 | Effect.gen(function*() { |
| 48 | const keyPair = yield* makeSigningKeyPair |
| 49 | const payload = makePayload(keyPair.signingPublicKey) |
| 50 | |
| 51 | const signature = yield* EventLogSessionAuth.signSessionAuthPayload({ |
| 52 | ...payload, |
| 53 | signingPrivateKey: keyPair.signingPrivateKey |
| 54 | }) |
| 55 | |
| 56 | assert.strictEqual(signature.byteLength, EventLogSessionAuth.Ed25519SignatureLength) |
| 57 | |
| 58 | const verified = yield* EventLogSessionAuth.verifySessionAuthPayload({ |
| 59 | ...payload, |
| 60 | signature |
| 61 | }) |
| 62 | |
| 63 | assert.strictEqual(verified, true) |
| 64 | })) |
| 65 | |
| 66 | it.effect("returns false for mismatched payload/signature", () => |
| 67 | Effect.gen(function*() { |
| 68 | const keyPair = yield* makeSigningKeyPair |
| 69 | const payload = makePayload(keyPair.signingPublicKey) |
| 70 | |
| 71 | const signature = yield* EventLogSessionAuth.signSessionAuthPayload({ |
| 72 | ...payload, |
| 73 | signingPrivateKey: keyPair.signingPrivateKey |
| 74 | }) |
| 75 | |
| 76 | const challenge = payload.challenge.slice() |
| 77 | challenge[0] = challenge[0] ^ 0xff |
| 78 | |
| 79 | const verified = yield* EventLogSessionAuth.verifySessionAuthPayload({ |
| 80 | ...payload, |
| 81 | challenge, |
| 82 | signature |
| 83 | }) |
| 84 | |
| 85 | assert.strictEqual(verified, false) |
| 86 | })) |
| 87 | |
| 88 | it.effect("rejects malformed signature length", () => |
| 89 | Effect.gen(function*() { |
| 90 | const keyPair = yield* makeSigningKeyPair |
| 91 | const payload = makePayload(keyPair.signingPublicKey) |
| 92 | |
| 93 | const error = yield* EventLogSessionAuth.verifySessionAuthPayload({ |
| 94 | ...payload, |
| 95 | signature: new Uint8Array(EventLogSessionAuth.Ed25519SignatureLength - 1) |
| 96 | }).pipe(Effect.flip) |
| 97 | |
| 98 | assert.strictEqual(error._tag, "EventLogSessionAuthError") |
| 99 | assert.strictEqual(error.reason, "InvalidSignatureLength") |
| 100 | })) |
| 101 | |
| 102 | it.effect("rejects malformed signing public key length", () => |
| 103 | Effect.gen(function*() { |
| 104 | const error = yield* EventLogSessionAuth.encodeSessionAuthPayload({ |
| 105 | remoteId: "remote-123", |
| 106 | challenge: new Uint8Array(32), |
| 107 | publicKey: "client-public-key", |
| 108 | signingPublicKey: new Uint8Array(EventLogSessionAuth.Ed25519PublicKeyLength - 1) |
| 109 | }).pipe(Effect.flip) |
| 110 | assert.strictEqual(error._tag, "EventLogSessionAuthError") |
| 111 | assert.strictEqual(error.reason, "InvalidSigningPublicKeyLength") |
| 112 | })) |
| 113 | |
| 114 | it.effect("rejects invalid canonical payload bytes", () => |
| 115 | Effect.gen(function*() { |
| 116 | const keyPair = yield* makeSigningKeyPair |
| 117 | const payload = makePayload(keyPair.signingPublicKey) |
| 118 | const encoded = yield* EventLogSessionAuth.encodeSessionAuthPayload(payload) |
| 119 | const malformed = encoded.slice(0, encoded.byteLength - 1) |
| 120 | |
| 121 | const error = yield* EventLogSessionAuth.verifySessionAuthPayloadBytes({ |
| 122 | payload: malformed, |
| 123 | signingPublicKey: payload.signingPublicKey, |
| 124 | signature: new Uint8Array(EventLogSessionAuth.Ed25519SignatureLength) |
| 125 | }).pipe(Effect.flip) |
| 126 | |
| 127 | assert.strictEqual(error._tag, "EventLogSessionAuthError") |
| 128 | assert.strictEqual(error.reason, "InvalidPayload") |
| 129 | })) |
| 130 | }) |