feat(casan): establish assurance kernel and harden control plane
This commit is contained in:
@@ -29,3 +29,17 @@ test('auth context fails closed to viewer for unknown role claim', () => {
|
||||
});
|
||||
assert.equal(actor.role, 'viewer');
|
||||
});
|
||||
|
||||
test('JWT mode rejects direct spoofed identity headers without middleware verification', () => {
|
||||
const prior = process.env.CASAN_CP_AUTH_MODE;
|
||||
process.env.CASAN_CP_AUTH_MODE = 'jwt';
|
||||
try {
|
||||
assert.throws(() => actorFromHeaders({
|
||||
'x-casan-actor': 'attacker',
|
||||
'x-casan-role': 'org-admin',
|
||||
}), /AUTH_VERIFIED_IDENTITY_REQUIRED/);
|
||||
} finally {
|
||||
if (prior === undefined) delete process.env.CASAN_CP_AUTH_MODE;
|
||||
else process.env.CASAN_CP_AUTH_MODE = prior;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { createHmac, createSign, generateKeyPairSync } from 'node:crypto';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
JwtAuthProvider,
|
||||
validateAuthConfig,
|
||||
type ControlPlaneAuthConfig,
|
||||
} from '../src/common/auth-provider.js';
|
||||
|
||||
const secret = 'test-only-secret-with-sufficient-length';
|
||||
const baseConfig: ControlPlaneAuthConfig = {
|
||||
mode: 'jwt',
|
||||
profile: 'test',
|
||||
bind: '0.0.0.0',
|
||||
issuer: 'https://issuer.test',
|
||||
audience: 'casan-control-plane',
|
||||
algorithm: 'HS256',
|
||||
hmacSecret: secret,
|
||||
clockSkewSeconds: 30,
|
||||
roleClaim: 'groups',
|
||||
tenantClaim: 'casan_tenant',
|
||||
projectClaim: 'casan_project',
|
||||
};
|
||||
|
||||
function token(payload: Record<string, unknown>, signingSecret = secret): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||
const signature = createHmac('sha256', signingSecret).update(`${header}.${body}`).digest('base64url');
|
||||
return `${header}.${body}.${signature}`;
|
||||
}
|
||||
|
||||
function rsToken(payload: Record<string, unknown>, privateKey: string): string {
|
||||
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
||||
const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||
const signer = createSign('RSA-SHA256');
|
||||
signer.update(`${header}.${body}`);
|
||||
signer.end();
|
||||
return `${header}.${body}.${signer.sign(privateKey).toString('base64url')}`;
|
||||
}
|
||||
|
||||
function claims(now: number, overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
iss: baseConfig.issuer,
|
||||
aud: baseConfig.audience,
|
||||
sub: 'verified-user',
|
||||
exp: now + 300,
|
||||
iat: now,
|
||||
groups: ['project-admin'],
|
||||
casan_tenant: 'tenant-a',
|
||||
casan_project: 'project-a',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('JWT provider verifies claims and ignores spoofed forwarded identity headers', () => {
|
||||
const now = 1_800_000_000;
|
||||
const provider = new JwtAuthProvider(baseConfig);
|
||||
const decision = provider.authenticate({
|
||||
authorization: `Bearer ${token(claims(now))}`,
|
||||
'x-forwarded-user': 'attacker',
|
||||
'x-casan-role': 'org-admin',
|
||||
'x-casan-tenant': 'victim',
|
||||
}, now);
|
||||
assert.equal(decision.allowed, true);
|
||||
assert.equal(decision.claims?.subject, 'verified-user');
|
||||
assert.equal(decision.claims?.tenant, 'tenant-a');
|
||||
assert.deepEqual(decision.claims?.roles, ['project-admin']);
|
||||
});
|
||||
|
||||
test('JWT provider rejects missing, invalid, expired, wrong-audience and wrong-issuer tokens', () => {
|
||||
const now = 1_800_000_000;
|
||||
const provider = new JwtAuthProvider(baseConfig);
|
||||
assert.equal(provider.authenticate({}, now).reasonCode, 'auth_bearer_token_required');
|
||||
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now), 'wrong-secret')}` }, now).reasonCode, 'auth_signature_invalid');
|
||||
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { exp: now - 31 }))}` }, now).reasonCode, 'auth_token_expired');
|
||||
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { aud: 'wrong' }))}` }, now).reasonCode, 'auth_audience_invalid');
|
||||
assert.equal(provider.authenticate({ authorization: `Bearer ${token(claims(now, { iss: 'https://wrong.test' }))}` }, now).reasonCode, 'auth_issuer_invalid');
|
||||
});
|
||||
|
||||
test('production-compatible RS256 verification accepts a valid asymmetric token', () => {
|
||||
const now = 1_800_000_000;
|
||||
const keys = generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
||||
});
|
||||
const provider = new JwtAuthProvider({
|
||||
...baseConfig,
|
||||
profile: 'production',
|
||||
algorithm: 'RS256',
|
||||
publicKey: keys.publicKey,
|
||||
hmacSecret: undefined,
|
||||
});
|
||||
const decision = provider.authenticate({ authorization: `Bearer ${rsToken(claims(now), keys.privateKey)}` }, now);
|
||||
assert.equal(decision.allowed, true);
|
||||
assert.equal(decision.claims?.subject, 'verified-user');
|
||||
});
|
||||
|
||||
test('production and non-loopback startup refuse local or symmetric identity modes', () => {
|
||||
assert.deepEqual(
|
||||
validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'production' }),
|
||||
['verified_identity_required'],
|
||||
);
|
||||
assert.ok(validateAuthConfig({ ...baseConfig, profile: 'production' }).includes('auth_asymmetric_key_required_in_production'));
|
||||
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'development' }).includes('verified_identity_required'));
|
||||
assert.ok(validateAuthConfig({ ...baseConfig, mode: 'local', profile: 'strict', bind: '127.0.0.1' }).includes('verified_identity_required'));
|
||||
assert.ok(validateAuthConfig({
|
||||
...baseConfig, profile: 'production', algorithm: 'RS256', publicKey: 'not-a-public-key', hmacSecret: undefined,
|
||||
}).includes('auth_rsa_public_key_invalid'));
|
||||
});
|
||||
Reference in New Issue
Block a user