264 lines
8.5 KiB
TypeScript
264 lines
8.5 KiB
TypeScript
|
|
/**
|
||
|
|
* P2 Protocol Conformance Tests
|
||
|
|
*
|
||
|
|
* Additional test vectors for JWT (RS256, ES256), HTTP Signature edge cases,
|
||
|
|
* and X.509/SPIFFE strictness beyond the base protocol-extensions.test.ts.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { test } from 'node:test'
|
||
|
|
import assert from 'node:assert'
|
||
|
|
import { createSign, generateKeyPairSync } from 'node:crypto'
|
||
|
|
import { jwtExtension } from '../../extensions/jwt.js'
|
||
|
|
import { httpSignatureExtension } from '../../extensions/http-signature.js'
|
||
|
|
import type { PredicateContext } from '../../extension/types.js'
|
||
|
|
|
||
|
|
const makeCtx = (overrides: Partial<PredicateContext['evalContext']> = {}): PredicateContext['evalContext'] => ({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: {},
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
response: {
|
||
|
|
body: undefined,
|
||
|
|
headers: {},
|
||
|
|
statusCode: 200,
|
||
|
|
},
|
||
|
|
...overrides,
|
||
|
|
} as PredicateContext['evalContext'])
|
||
|
|
|
||
|
|
const makeRoute = () => ({
|
||
|
|
path: '/test',
|
||
|
|
method: 'GET' as const,
|
||
|
|
category: 'observer' as const,
|
||
|
|
requires: [],
|
||
|
|
ensures: [],
|
||
|
|
invariants: [],
|
||
|
|
regexPatterns: {},
|
||
|
|
validateRuntime: true,
|
||
|
|
})
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// JWT: RS256 and ES256 verification vectors
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('jwt: validates RS256 signature with RSA public key', () => {
|
||
|
|
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||
|
|
const payload = { sub: 'user-123', iat: Math.floor(Date.now() / 1000) }
|
||
|
|
const header = { alg: 'RS256', typ: 'JWT' }
|
||
|
|
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url')
|
||
|
|
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||
|
|
const signingInput = `${encodedHeader}.${encodedPayload}`
|
||
|
|
const signer = createSign('RSA-SHA256')
|
||
|
|
signer.update(signingInput)
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64url')
|
||
|
|
const token = `${signingInput}.${signature}`
|
||
|
|
|
||
|
|
const ext = jwtExtension({
|
||
|
|
keys: { default: publicKey.export({ type: 'spki', format: 'pem' }).toString() },
|
||
|
|
verify: true,
|
||
|
|
})
|
||
|
|
const state = ext.onSuiteStart!({}) as Record<string, unknown>
|
||
|
|
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: { authorization: `Bearer ${token}` },
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: state,
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.jwt_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('jwt: rejects RS256 token with wrong public key', () => {
|
||
|
|
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||
|
|
const { publicKey: wrongPublicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||
|
|
const payload = { sub: 'user-123', iat: Math.floor(Date.now() / 1000) }
|
||
|
|
const header = { alg: 'RS256', typ: 'JWT' }
|
||
|
|
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url')
|
||
|
|
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||
|
|
const signingInput = `${encodedHeader}.${encodedPayload}`
|
||
|
|
const signer = createSign('RSA-SHA256')
|
||
|
|
signer.update(signingInput)
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64url')
|
||
|
|
const token = `${signingInput}.${signature}`
|
||
|
|
|
||
|
|
const ext = jwtExtension({
|
||
|
|
keys: { default: wrongPublicKey.export({ type: 'spki', format: 'pem' }).toString() },
|
||
|
|
verify: true,
|
||
|
|
})
|
||
|
|
const state = ext.onSuiteStart!({}) as Record<string, unknown>
|
||
|
|
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: { authorization: `Bearer ${token}` },
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: state,
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.jwt_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, false)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('jwt: validates ES256 signature with EC public key', () => {
|
||
|
|
const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
|
||
|
|
const payload = { sub: 'user-123', iat: Math.floor(Date.now() / 1000) }
|
||
|
|
const header = { alg: 'ES256', typ: 'JWT' }
|
||
|
|
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url')
|
||
|
|
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||
|
|
const signingInput = `${encodedHeader}.${encodedPayload}`
|
||
|
|
const signer = createSign('SHA256')
|
||
|
|
signer.update(signingInput)
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64url')
|
||
|
|
const token = `${signingInput}.${signature}`
|
||
|
|
|
||
|
|
const ext = jwtExtension({
|
||
|
|
keys: { default: publicKey.export({ type: 'spki', format: 'pem' }).toString() },
|
||
|
|
verify: true,
|
||
|
|
})
|
||
|
|
const state = ext.onSuiteStart!({}) as Record<string, unknown>
|
||
|
|
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: { authorization: `Bearer ${token}` },
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: state,
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.jwt_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, true)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('jwt: rejects ES256 token with wrong public key', () => {
|
||
|
|
const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
|
||
|
|
const { publicKey: wrongPublicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
|
||
|
|
const payload = { sub: 'user-123', iat: Math.floor(Date.now() / 1000) }
|
||
|
|
const header = { alg: 'ES256', typ: 'JWT' }
|
||
|
|
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url')
|
||
|
|
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||
|
|
const signingInput = `${encodedHeader}.${encodedPayload}`
|
||
|
|
const signer = createSign('SHA256')
|
||
|
|
signer.update(signingInput)
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64url')
|
||
|
|
const token = `${signingInput}.${signature}`
|
||
|
|
|
||
|
|
const ext = jwtExtension({
|
||
|
|
keys: { default: wrongPublicKey.export({ type: 'spki', format: 'pem' }).toString() },
|
||
|
|
verify: true,
|
||
|
|
})
|
||
|
|
const state = ext.onSuiteStart!({}) as Record<string, unknown>
|
||
|
|
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: { authorization: `Bearer ${token}` },
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: state,
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.jwt_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, false)
|
||
|
|
})
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// HTTP Signature: negative corpus and edge cases
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test('httpSignature: rejects unsupported signature algorithm', () => {
|
||
|
|
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||
|
|
const signatureInput = 'sig1=("@method")'
|
||
|
|
const signer = createSign('SHA512')
|
||
|
|
signer.update('dummy')
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64')
|
||
|
|
|
||
|
|
const ext = httpSignatureExtension()
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: {
|
||
|
|
signature: `sig1=:${signature}:`,
|
||
|
|
'signature-input': signatureInput,
|
||
|
|
},
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: {},
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.signature_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, false)
|
||
|
|
})
|
||
|
|
|
||
|
|
test('httpSignature: rejects signature with mismatched label', () => {
|
||
|
|
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
||
|
|
const ext = httpSignatureExtension({ publicKey: publicKey.export({ type: 'spki', format: 'pem' }).toString() })
|
||
|
|
const signatureInput = 'sig1=("@method")'
|
||
|
|
const signer = createSign('SHA256')
|
||
|
|
signer.update('dummy')
|
||
|
|
signer.end()
|
||
|
|
const signature = signer.sign(privateKey).toString('base64')
|
||
|
|
|
||
|
|
const ctx: PredicateContext = {
|
||
|
|
route: makeRoute(),
|
||
|
|
evalContext: makeCtx({
|
||
|
|
request: {
|
||
|
|
body: undefined,
|
||
|
|
headers: {
|
||
|
|
signature: `sig2=:${signature}:`,
|
||
|
|
'signature-input': signatureInput,
|
||
|
|
},
|
||
|
|
query: {},
|
||
|
|
params: {},
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
accessor: [],
|
||
|
|
extensionState: {},
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = ext.predicates!.signature_valid!(ctx)
|
||
|
|
assert.ok(result.success)
|
||
|
|
assert.strictEqual(result.value, false)
|
||
|
|
})
|