feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation
This commit is contained in:
209
test/server/test.chap.ts
Normal file
209
test/server/test.chap.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as crypto from 'crypto';
|
||||
import { RadiusAuthenticator } from '../../ts_server/index.js';
|
||||
|
||||
tap.test('should calculate CHAP response per RFC', async () => {
|
||||
// CHAP-Response = MD5(CHAP-ID + Password + Challenge)
|
||||
const chapId = 42;
|
||||
const password = 'mypassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
// Verify manually
|
||||
const md5 = crypto.createHash('md5');
|
||||
md5.update(Buffer.from([chapId]));
|
||||
md5.update(Buffer.from(password, 'utf8'));
|
||||
md5.update(challenge);
|
||||
const expected = md5.digest();
|
||||
|
||||
expect(response.length).toEqual(16);
|
||||
expect(response.equals(expected)).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should verify valid CHAP response', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'correctpassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
// Calculate the expected response
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
// Build CHAP-Password attribute: CHAP Ident (1 byte) + Response (16 bytes)
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
// Should verify with correct password
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should reject CHAP response with wrong password', async () => {
|
||||
const chapId = 1;
|
||||
const correctPassword = 'correctpassword';
|
||||
const wrongPassword = 'wrongpassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
// Calculate response with correct password
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, correctPassword, challenge);
|
||||
|
||||
// Build CHAP-Password attribute
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
// Should NOT verify with wrong password
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, wrongPassword);
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should reject CHAP response with wrong challenge', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'mypassword';
|
||||
const correctChallenge = crypto.randomBytes(16);
|
||||
const wrongChallenge = crypto.randomBytes(16);
|
||||
|
||||
// Calculate response with correct challenge
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, correctChallenge);
|
||||
|
||||
// Build CHAP-Password attribute
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
// Should NOT verify with wrong challenge
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, wrongChallenge, password);
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should reject CHAP response with wrong identifier', async () => {
|
||||
const correctChapId = 1;
|
||||
const wrongChapId = 2;
|
||||
const password = 'mypassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
// Calculate response with correct CHAP ID
|
||||
const response = RadiusAuthenticator.calculateChapResponse(correctChapId, password, challenge);
|
||||
|
||||
// Build CHAP-Password with WRONG CHAP ID
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(wrongChapId, 0); // Wrong ID
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
// Should NOT verify
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should reject invalid CHAP-Password length', async () => {
|
||||
const challenge = crypto.randomBytes(16);
|
||||
const password = 'mypassword';
|
||||
|
||||
// CHAP-Password must be exactly 17 bytes (1 + 16)
|
||||
const invalidChapPassword = Buffer.alloc(16); // Too short
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(invalidChapPassword, challenge, password);
|
||||
expect(isValid).toBeFalsy();
|
||||
|
||||
const tooLongChapPassword = Buffer.alloc(18); // Too long
|
||||
const isValid2 = RadiusAuthenticator.verifyChapResponse(tooLongChapPassword, challenge, password);
|
||||
expect(isValid2).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should handle all CHAP ID values (0-255)', async () => {
|
||||
const password = 'testpassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
for (const chapId of [0, 1, 127, 128, 254, 255]) {
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should handle special characters in password', async () => {
|
||||
const chapId = 1;
|
||||
const password = '!@#$%^&*()_+-=[]{}|;:,.<>?~`';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle unicode password', async () => {
|
||||
const chapId = 1;
|
||||
const password = '密码パスワード'; // Chinese + Japanese
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle empty password', async () => {
|
||||
const chapId = 1;
|
||||
const password = '';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle very long password', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'a'.repeat(1000); // Very long password
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should handle different challenge lengths', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'testpassword';
|
||||
|
||||
// CHAP challenge can be various lengths
|
||||
for (const length of [8, 16, 24, 32]) {
|
||||
const challenge = crypto.randomBytes(length);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user