1import { assert, describe, it } from "@effect/vitest"
2import { Effect } from "effect"
3import * as EventLogSessionAuth from "effect/eventlog/EventLogSessionAuth"
4
5const 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
18const 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
25describe("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})