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:
205
test/server/test.authenticator.ts
Normal file
205
test/server/test.authenticator.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as crypto from 'crypto';
|
||||
import {
|
||||
RadiusAuthenticator,
|
||||
RadiusPacket,
|
||||
ERadiusCode,
|
||||
ERadiusAttributeType,
|
||||
} from '../../ts_server/index.js';
|
||||
|
||||
tap.test('should generate 16-byte random request authenticator', async () => {
|
||||
const auth1 = RadiusAuthenticator.generateRequestAuthenticator();
|
||||
const auth2 = RadiusAuthenticator.generateRequestAuthenticator();
|
||||
|
||||
expect(auth1.length).toEqual(16);
|
||||
expect(auth2.length).toEqual(16);
|
||||
|
||||
// Should be random (different each time)
|
||||
expect(auth1.equals(auth2)).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should calculate response authenticator correctly', async () => {
|
||||
const code = ERadiusCode.AccessAccept;
|
||||
const identifier = 1;
|
||||
const requestAuthenticator = crypto.randomBytes(16);
|
||||
const attributes = Buffer.from([]);
|
||||
const secret = 'testing123';
|
||||
|
||||
const responseAuth = RadiusAuthenticator.calculateResponseAuthenticator(
|
||||
code,
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
attributes,
|
||||
secret
|
||||
);
|
||||
|
||||
expect(responseAuth.length).toEqual(16);
|
||||
|
||||
// Verify by recalculating
|
||||
const verified = RadiusAuthenticator.calculateResponseAuthenticator(
|
||||
code,
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
attributes,
|
||||
secret
|
||||
);
|
||||
|
||||
expect(responseAuth.equals(verified)).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should calculate accounting request authenticator', async () => {
|
||||
const code = ERadiusCode.AccountingRequest;
|
||||
const identifier = 1;
|
||||
const attributes = Buffer.from([]);
|
||||
const secret = 'testing123';
|
||||
|
||||
const acctAuth = RadiusAuthenticator.calculateAccountingRequestAuthenticator(
|
||||
code,
|
||||
identifier,
|
||||
attributes,
|
||||
secret
|
||||
);
|
||||
|
||||
expect(acctAuth.length).toEqual(16);
|
||||
});
|
||||
|
||||
tap.test('should verify accounting request authenticator', async () => {
|
||||
const secret = 'testing123';
|
||||
|
||||
// Create an accounting request packet
|
||||
const packet = RadiusPacket.createAccountingRequest(1, secret, [
|
||||
{ type: ERadiusAttributeType.AcctStatusType, value: 1 }, // Start
|
||||
{ type: ERadiusAttributeType.AcctSessionId, value: 'session-123' },
|
||||
]);
|
||||
|
||||
// Verify the authenticator
|
||||
const isValid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, secret);
|
||||
expect(isValid).toBeTruthy();
|
||||
|
||||
// Should fail with wrong secret
|
||||
const isInvalid = RadiusAuthenticator.verifyAccountingRequestAuthenticator(packet, 'wrongsecret');
|
||||
expect(isInvalid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should verify response authenticator', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuthenticator = crypto.randomBytes(16);
|
||||
const secret = 'testing123';
|
||||
|
||||
// Create a response packet
|
||||
const responsePacket = RadiusPacket.createAccessAccept(
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
secret,
|
||||
[{ type: ERadiusAttributeType.ReplyMessage, value: 'Welcome' }]
|
||||
);
|
||||
|
||||
// Verify the authenticator
|
||||
const isValid = RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
responsePacket,
|
||||
requestAuthenticator,
|
||||
secret
|
||||
);
|
||||
expect(isValid).toBeTruthy();
|
||||
|
||||
// Should fail with wrong request authenticator
|
||||
const wrongRequestAuth = crypto.randomBytes(16);
|
||||
const isInvalid = RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
responsePacket,
|
||||
wrongRequestAuth,
|
||||
secret
|
||||
);
|
||||
expect(isInvalid).toBeFalsy();
|
||||
|
||||
// Should fail with wrong secret
|
||||
const isInvalid2 = RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
responsePacket,
|
||||
requestAuthenticator,
|
||||
'wrongsecret'
|
||||
);
|
||||
expect(isInvalid2).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should create packet header', async () => {
|
||||
const code = ERadiusCode.AccessRequest;
|
||||
const identifier = 42;
|
||||
const authenticator = crypto.randomBytes(16);
|
||||
const attributesLength = 50;
|
||||
|
||||
const header = RadiusAuthenticator.createPacketHeader(
|
||||
code,
|
||||
identifier,
|
||||
authenticator,
|
||||
attributesLength
|
||||
);
|
||||
|
||||
expect(header.length).toEqual(20);
|
||||
expect(header[0]).toEqual(code);
|
||||
expect(header[1]).toEqual(identifier);
|
||||
expect(header.readUInt16BE(2)).toEqual(20 + attributesLength);
|
||||
expect(header.subarray(4, 20).equals(authenticator)).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should calculate CHAP response', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'testpassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
expect(response.length).toEqual(16);
|
||||
|
||||
// Verify the calculation: MD5(CHAP-ID + Password + Challenge)
|
||||
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.equals(expected)).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('should verify CHAP response', async () => {
|
||||
const chapId = 1;
|
||||
const password = 'testpassword';
|
||||
const challenge = crypto.randomBytes(16);
|
||||
|
||||
// Calculate the response
|
||||
const response = RadiusAuthenticator.calculateChapResponse(chapId, password, challenge);
|
||||
|
||||
// Create CHAP-Password attribute format: CHAP-ID (1 byte) + Response (16 bytes)
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
response.copy(chapPassword, 1);
|
||||
|
||||
// Verify with correct password
|
||||
const isValid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, password);
|
||||
expect(isValid).toBeTruthy();
|
||||
|
||||
// Verify with wrong password
|
||||
const isInvalid = RadiusAuthenticator.verifyChapResponse(chapPassword, challenge, 'wrongpassword');
|
||||
expect(isInvalid).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should calculate Message-Authenticator (HMAC-MD5)', async () => {
|
||||
const packet = Buffer.alloc(50);
|
||||
packet[0] = ERadiusCode.AccessRequest;
|
||||
packet[1] = 1;
|
||||
packet.writeUInt16BE(50, 2);
|
||||
crypto.randomBytes(16).copy(packet, 4);
|
||||
|
||||
const secret = 'testing123';
|
||||
|
||||
const msgAuth = RadiusAuthenticator.calculateMessageAuthenticator(packet, secret);
|
||||
|
||||
expect(msgAuth.length).toEqual(16);
|
||||
|
||||
// Verify it's HMAC-MD5
|
||||
const hmac = crypto.createHmac('md5', Buffer.from(secret, 'utf8'));
|
||||
hmac.update(packet);
|
||||
const expected = hmac.digest();
|
||||
|
||||
expect(msgAuth.equals(expected)).toBeTruthy();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user