SIP Authentication and Security
Master SIP authentication mechanisms (HTTP Digest), TLS encryption, SIPS, and security best practices for building secure VoIP applications.
HTTP Digest Authentication
Challenge-Response Flow
Client Server | | | REGISTER (no credentials) | |---------------------------------------->| | | | 401 Unauthorized | | WWW-Authenticate: Digest | | realm="atlanta.com" | | nonce="dcd98b7102dd..." | | algorithm=MD5 | | qop="auth" | |<----------------------------------------| | | | REGISTER (with Authorization) | | Authorization: Digest | | username="alice" | | realm="atlanta.com" | | nonce="dcd98b7102dd..." | | uri="sip:atlanta.com" | | response="6629fae49393..." | | algorithm=MD5 | | qop=auth | | nc=00000001 | | cnonce="0a4f113b" | |---------------------------------------->| | | | 200 OK | |<----------------------------------------| | |
Digest Authentication Implementation
import crypto from 'crypto';
interface DigestChallenge { realm: string; nonce: string; algorithm: 'MD5' | 'SHA-256'; qop?: 'auth' | 'auth-int'; opaque?: string; stale?: boolean; }
interface DigestCredentials { username: string; realm: string; nonce: string; uri: string; response: string; algorithm: 'MD5' | 'SHA-256'; cnonce?: string; nc?: string; qop?: string; opaque?: string; }
class SipDigestAuth { // Generate authentication challenge (401/407 response) static generateChallenge(realm: string): DigestChallenge { return { realm, nonce: this.generateNonce(), algorithm: 'MD5', qop: 'auth', opaque: this.generateOpaque() }; }
// Create WWW-Authenticate or Proxy-Authenticate header
static createChallengeHeader(challenge: DigestChallenge): string {
let header = Digest realm="${challenge.realm}", +
nonce="${challenge.nonce}", +
algorithm=${challenge.algorithm};
if (challenge.qop) {
header += `, qop="${challenge.qop}"`;
}
if (challenge.opaque) {
header += `, opaque="${challenge.opaque}"`;
}
if (challenge.stale) {
header += `, stale=TRUE`;
}
return header;
}
// Calculate response for authentication static calculateResponse(params: { username: string; password: string; realm: string; method: string; uri: string; nonce: string; algorithm?: 'MD5' | 'SHA-256'; cnonce?: string; nc?: string; qop?: string; body?: string; }): string { const algorithm = params.algorithm || 'MD5'; const hashFunc = algorithm === 'MD5' ? 'md5' : 'sha256';
// Calculate A1 = MD5(username:realm:password)
const a1 = this.hash(
hashFunc,
`${params.username}:${params.realm}:${params.password}`
);
// Calculate A2
let a2: string;
if (params.qop === 'auth-int') {
// A2 = MD5(method:uri:MD5(body))
const bodyHash = this.hash(hashFunc, params.body || '');
a2 = this.hash(hashFunc, `${params.method}:${params.uri}:${bodyHash}`);
} else {
// A2 = MD5(method:uri)
a2 = this.hash(hashFunc, `${params.method}:${params.uri}`);
}
// Calculate response
let response: string;
if (params.qop) {
// response = MD5(A1:nonce:nc:cnonce:qop:A2)
response = this.hash(
hashFunc,
`${a1}:${params.nonce}:${params.nc}:${params.cnonce}:${params.qop}:${a2}`
);
} else {
// response = MD5(A1:nonce:A2)
response = this.hash(hashFunc, `${a1}:${params.nonce}:${a2}`);
}
return response;
}
// Create Authorization or Proxy-Authorization header static createAuthorizationHeader(params: { username: string; password: string; realm: string; method: string; uri: string; nonce: string; algorithm?: 'MD5' | 'SHA-256'; qop?: string; opaque?: string; }): string { const algorithm = params.algorithm || 'MD5'; const cnonce = this.generateCnonce(); const nc = '00000001'; const qop = params.qop || 'auth';
const response = this.calculateResponse({
username: params.username,
password: params.password,
realm: params.realm,
method: params.method,
uri: params.uri,
nonce: params.nonce,
algorithm,
cnonce,
nc,
qop
});
let header = `Digest username="${params.username}", ` +
`realm="${params.realm}", ` +
`nonce="${params.nonce}", ` +
`uri="${params.uri}", ` +
`response="${response}", ` +
`algorithm=${algorithm}`;
if (qop) {
header += `, qop=${qop}, nc=${nc}, cnonce="${cnonce}"`;
}
if (params.opaque) {
header += `, opaque="${params.opaque}"`;
}
return header;
}
// Verify client credentials static verifyCredentials( credentials: DigestCredentials, password: string, method: string ): boolean { const expectedResponse = this.calculateResponse({ username: credentials.username, password, realm: credentials.realm, method, uri: credentials.uri, nonce: credentials.nonce, algorithm: credentials.algorithm, cnonce: credentials.cnonce, nc: credentials.nc, qop: credentials.qop });
return credentials.response === expectedResponse;
}
// Parse Authorization/Proxy-Authorization header static parseAuthorizationHeader(header: string): DigestCredentials | null { if (!header.startsWith('Digest ')) { return null; }
const params: any = {};
const paramRegex = /(\w+)=(?:"([^"]+)"|([^,\s]+))/g;
let match;
while ((match = paramRegex.exec(header)) !== null) {
const key = match[1];
const value = match[2] || match[3];
params[key] = value;
}
return {
username: params.username,
realm: params.realm,
nonce: params.nonce,
uri: params.uri,
response: params.response,
algorithm: params.algorithm || 'MD5',
cnonce: params.cnonce,
nc: params.nc,
qop: params.qop,
opaque: params.opaque
};
}
private static hash(algorithm: string, data: string): string { return crypto.createHash(algorithm).update(data).digest('hex'); }
private static generateNonce(): string {
// Nonce = Base64(timestamp:ETag:private-key)
const timestamp = Date.now();
const etag = crypto.randomBytes(16).toString('hex');
const privateKey = 'secret-server-key';
const nonce = ${timestamp}:${etag}:${privateKey};
return Buffer.from(nonce).toString('base64');
}
private static generateCnonce(): string { return crypto.randomBytes(16).toString('hex'); }
private static generateOpaque(): string { return crypto.randomBytes(16).toString('hex'); } }
Complete Authentication Example
class SipAuthenticatedClient { private username: string; private password: string; private realm?: string; private nonce?: string; private opaque?: string;
constructor(username: string, password: string) { this.username = username; this.password = password; }
// Send REGISTER with authentication async register(server: string): Promise<void> { // First attempt without credentials let response = await this.sendRegister(server);
if (response.statusCode === 401) {
// Extract challenge from WWW-Authenticate header
const challenge = this.parseChallenge(response.headers['www-authenticate']);
if (!challenge) {
throw new Error('Invalid authentication challenge');
}
this.realm = challenge.realm;
this.nonce = challenge.nonce;
this.opaque = challenge.opaque;
// Send REGISTER with credentials
response = await this.sendRegister(server, true);
}
if (response.statusCode === 200) {
console.log('Registration successful');
} else {
throw new Error(`Registration failed: ${response.statusCode}`);
}
}
private async sendRegister(
server: string,
withAuth: boolean = false
): Promise<any> {
const uri = sip:${server};
const method = 'REGISTER';
let message = `${method} ${uri} SIP/2.0\r
Via: SIP/2.0/UDP client.example.com;branch=z9hG4bK${this.generateBranch()}\r Max-Forwards: 70\r To: <sip:${this.username}@${server}>\r From: <sip:${this.username}@${server}>;tag=${this.generateTag()}\r Call-ID: ${this.generateCallId()}\r CSeq: 1 REGISTER\r Contact: <sip:${this.username}@client.example.com>\r Expires: 3600\r `;
if (withAuth && this.realm && this.nonce) {
const authHeader = SipDigestAuth.createAuthorizationHeader({
username: this.username,
password: this.password,
realm: this.realm,
method,
uri,
nonce: this.nonce,
opaque: this.opaque
});
message += `Authorization: ${authHeader}\r\n`;
}
message += 'Content-Length: 0\r\n\r\n';
// Send message and get response
return this.send(message);
}
private parseChallenge(header: string): DigestChallenge | null { if (!header || !header.startsWith('Digest ')) { return null; }
const params: any = {};
const paramRegex = /(\w+)=(?:"([^"]+)"|([^,\s]+))/g;
let match;
while ((match = paramRegex.exec(header)) !== null) {
const key = match[1];
const value = match[2] || match[3];
params[key] = value;
}
return {
realm: params.realm,
nonce: params.nonce,
algorithm: params.algorithm || 'MD5',
qop: params.qop,
opaque: params.opaque
};
}
private generateBranch(): string { return crypto.randomBytes(16).toString('hex'); }
private generateTag(): string { return crypto.randomBytes(8).toString('hex'); }
private generateCallId(): string {
return ${crypto.randomBytes(16).toString('hex')}@client.example.com;
}
private async send(message: string): Promise<any> { // Implementation depends on transport console.log('Sending:', message); return { statusCode: 401, headers: {} }; } }
Server-Side Authentication
Registration Server with Authentication
interface UserCredentials { username: string; password: string; domain: string; }
class SipRegistrar { private users: Map<string, UserCredentials> = new Map(); private registrations: Map<string, Registration> = new Map(); private nonces: Map<string, NonceInfo> = new Map(); private realm: string;
constructor(realm: string) { this.realm = realm; }
// Add user to database addUser(username: string, password: string, domain: string): void { this.users.set(username, { username, password, domain }); }
// Handle REGISTER request handleRegister(request: SipRequest): SipResponse { const authHeader = request.headers['authorization'];
if (!authHeader) {
// No credentials, send challenge
return this.sendChallenge();
}
// Parse credentials
const credentials = SipDigestAuth.parseAuthorizationHeader(authHeader);
if (!credentials) {
return this.createResponse(400, 'Bad Request');
}
// Verify nonce
const nonceInfo = this.nonces.get(credentials.nonce);
if (!nonceInfo) {
// Nonce expired or invalid, send new challenge
return this.sendChallenge(true);
}
// Check nonce count to prevent replay attacks
if (credentials.nc && parseInt(credentials.nc, 16) <= nonceInfo.nc) {
return this.createResponse(401, 'Unauthorized');
}
// Get user password
const user = this.users.get(credentials.username);
if (!user) {
return this.createResponse(403, 'Forbidden');
}
// Verify credentials
const valid = SipDigestAuth.verifyCredentials(
credentials,
user.password,
'REGISTER'
);
if (!valid) {
return this.createResponse(403, 'Forbidden');
}
// Update nonce count
if (credentials.nc) {
nonceInfo.nc = parseInt(credentials.nc, 16);
}
// Register contact
const contact = request.headers['contact'];
const expires = parseInt(request.headers['expires'] || '3600');
this.registerContact(credentials.username, contact, expires);
return this.createResponse(200, 'OK');
}
private sendChallenge(stale: boolean = false): SipResponse { const challenge = SipDigestAuth.generateChallenge(this.realm); challenge.stale = stale;
// Store nonce
this.nonces.set(challenge.nonce, {
nonce: challenge.nonce,
timestamp: Date.now(),
nc: 0
});
const response = this.createResponse(401, 'Unauthorized');
response.headers['www-authenticate'] =
SipDigestAuth.createChallengeHeader(challenge);
return response;
}
private registerContact( username: string, contact: string, expires: number ): void { const registration: Registration = { username, contact, expires: Date.now() + expires * 1000 };
this.registrations.set(username, registration);
// Set expiration timer
setTimeout(() => {
this.registrations.delete(username);
}, expires * 1000);
}
private createResponse(statusCode: number, reason: string): SipResponse { return { version: 'SIP/2.0', statusCode, reasonPhrase: reason, headers: {} as any, body: undefined }; }
// Clean up expired nonces cleanupNonces(): void { const now = Date.now(); const maxAge = 300000; // 5 minutes
for (const [nonce, info] of this.nonces.entries()) {
if (now - info.timestamp > maxAge) {
this.nonces.delete(nonce);
}
}
} }
interface Registration { username: string; contact: string; expires: number; }
interface NonceInfo { nonce: string; timestamp: number; nc: number; }
interface SipRequest { method: string; requestUri: string; version: string; headers: any; body?: string; }
interface SipResponse { version: string; statusCode: number; reasonPhrase: string; headers: any; body?: string; }
TLS/SIPS Implementation
Secure SIP (SIPS) Setup
import tls from 'tls'; import fs from 'fs';
interface TlsConfig { cert: string; key: string; ca?: string; rejectUnauthorized?: boolean; minVersion?: string; ciphers?: string; }
class SipTlsServer { private server: tls.Server; private config: TlsConfig;
constructor(config: TlsConfig) { this.config = config; this.server = this.createServer(); }
private createServer(): tls.Server { const options: tls.TlsOptions = { cert: fs.readFileSync(this.config.cert), key: fs.readFileSync(this.config.key), // Require client certificate for mutual TLS requestCert: true, rejectUnauthorized: this.config.rejectUnauthorized !== false, // Use strong TLS version minVersion: (this.config.minVersion as any) || 'TLSv1.2', // Use secure cipher suites ciphers: this.config.ciphers || [ 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384', 'DHE-RSA-AES128-GCM-SHA256' ].join(':') };
if (this.config.ca) {
options.ca = fs.readFileSync(this.config.ca);
}
const server = tls.createServer(options, (socket) => {
this.handleConnection(socket);
});
return server;
}
private handleConnection(socket: tls.TLSSocket): void { console.log('Secure connection established');
// Verify client certificate
if (socket.authorized) {
console.log('Client certificate verified');
const cert = socket.getPeerCertificate();
console.log('Client CN:', cert.subject.CN);
} else {
console.log('Client certificate not authorized:', socket.authorizationError);
socket.destroy();
return;
}
socket.on('data', (data) => {
this.handleData(socket, data);
});
socket.on('error', (error) => {
console.error('Socket error:', error);
});
socket.on('close', () => {
console.log('Connection closed');
});
}
private handleData(socket: tls.TLSSocket, data: Buffer): void { const message = data.toString(); console.log('Received:', message);
// Process SIP message
// Send response
}
listen(port: number, host: string = '0.0.0.0'): void {
this.server.listen(port, host, () => {
console.log(SIP TLS server listening on ${host}:${port});
});
}
close(): void { this.server.close(); } }
class SipTlsClient { private config: TlsConfig;
constructor(config: TlsConfig) { this.config = config; }
connect(host: string, port: number): Promise<tls.TLSSocket> { return new Promise((resolve, reject) => { const options: tls.ConnectionOptions = { host, port, cert: fs.readFileSync(this.config.cert), key: fs.readFileSync(this.config.key), rejectUnauthorized: this.config.rejectUnauthorized !== false, minVersion: (this.config.minVersion as any) || 'TLSv1.2', ciphers: this.config.ciphers };
if (this.config.ca) {
options.ca = fs.readFileSync(this.config.ca);
}
const socket = tls.connect(options, () => {
if (socket.authorized) {
console.log('Connected to server, certificate verified');
resolve(socket);
} else {
console.error('Certificate verification failed:', socket.authorizationError);
socket.destroy();
reject(new Error('Certificate verification failed'));
}
});
socket.on('error', (error) => {
reject(error);
});
});
}
async send(host: string, port: number, message: string): Promise<void> { const socket = await this.connect(host, port);
socket.write(message);
socket.on('data', (data) => {
console.log('Received:', data.toString());
});
} }
// Usage example const serverConfig: TlsConfig = { cert: '/path/to/server-cert.pem', key: '/path/to/server-key.pem', ca: '/path/to/ca-cert.pem', rejectUnauthorized: true };
const server = new SipTlsServer(serverConfig); server.listen(5061);
const clientConfig: TlsConfig = { cert: '/path/to/client-cert.pem', key: '/path/to/client-key.pem', ca: '/path/to/ca-cert.pem' };
const client = new SipTlsClient(clientConfig);
SRTP and Media Security
SRTP Key Exchange in SDP
v=0 o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.com s=Secure Session c=IN IP4 pc33.atlanta.com t=0 0 m=audio 49170 RTP/SAVP 0 a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 a=crypto:2 AES_CM_128_HMAC_SHA1_32 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 a=rtpmap:0 PCMU/8000
SRTP Implementation
import crypto from 'crypto';
interface SrtpParams { cryptoSuite: string; keyParams: string; sessionParams?: string; }
class SrtpCrypto { // Parse crypto attribute from SDP static parseCryptoAttribute(attr: string): SrtpParams | null { // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:base64key|lifetime|mkiValue const match = attr.match( /crypto:(\d+)\s+([^\s]+)\s+inline:([^\s|]+)(?:|([^\s|]+))?(?:|([^\s]+))?/ );
if (!match) {
return null;
}
const [, tag, cryptoSuite, keyParams, lifetime, mki] = match;
return {
cryptoSuite,
keyParams,
sessionParams: lifetime
};
}
// Generate crypto attribute for SDP static generateCryptoAttribute(tag: number = 1): string { const cryptoSuite = 'AES_CM_128_HMAC_SHA1_80'; const masterKey = crypto.randomBytes(16); // 128 bits const masterSalt = crypto.randomBytes(14); // 112 bits
// Concatenate key and salt
const keyMaterial = Buffer.concat([masterKey, masterSalt]);
const keyParams = keyMaterial.toString('base64');
// Session parameters
const lifetime = '2^20'; // 2^20 packets
const mki = '1:32'; // MKI value:length
return `crypto:${tag} ${cryptoSuite} inline:${keyParams}|${lifetime}|${mki}`;
}
// Derive session keys from master key static deriveSessionKeys( masterKey: Buffer, masterSalt: Buffer, index: number ): { encryptionKey: Buffer; authKey: Buffer; saltingKey: Buffer; } { // Key derivation according to RFC 3711 const kdr = 0; // Key derivation rate (0 = never rekeyed)
// Calculate key_id
const r = index / (2 ** kdr);
// Derive encryption key
const encryptionKey = this.deriveKey(masterKey, masterSalt, 0x00, r);
// Derive authentication key
const authKey = this.deriveKey(masterKey, masterSalt, 0x01, r);
// Derive salting key
const saltingKey = this.deriveKey(masterKey, masterSalt, 0x02, r);
return { encryptionKey, authKey, saltingKey };
}
private static deriveKey( masterKey: Buffer, masterSalt: Buffer, label: number, index: number ): Buffer { // PRF(masterKey, (masterSalt XOR (label || index))) // Simplified implementation const iv = Buffer.alloc(16); masterSalt.copy(iv); iv[7] ^= label;
const cipher = crypto.createCipheriv('aes-128-cbc', masterKey, iv);
const key = cipher.update(Buffer.alloc(16));
return key;
}
// Encrypt RTP packet static encryptRtp( packet: Buffer, encryptionKey: Buffer, saltingKey: Buffer, ssrc: number, sequenceNumber: number ): Buffer { // Extract RTP header (first 12 bytes) const header = packet.slice(0, 12);
// Extract payload
const payload = packet.slice(12);
// Generate IV from salting key and packet index
const iv = this.generateIv(saltingKey, ssrc, sequenceNumber);
// Encrypt payload
const cipher = crypto.createCipheriv('aes-128-ctr', encryptionKey, iv);
const encryptedPayload = Buffer.concat([
cipher.update(payload),
cipher.final()
]);
// Concatenate header and encrypted payload
return Buffer.concat([header, encryptedPayload]);
}
// Generate authentication tag static generateAuthTag( packet: Buffer, authKey: Buffer ): Buffer { const hmac = crypto.createHmac('sha1', authKey); hmac.update(packet); const tag = hmac.digest();
// Use first 10 bytes for AES_CM_128_HMAC_SHA1_80
return tag.slice(0, 10);
}
private static generateIv( saltingKey: Buffer, ssrc: number, sequenceNumber: number ): Buffer { const iv = Buffer.alloc(16);
// Copy salting key
saltingKey.copy(iv);
// XOR with SSRC
iv.writeUInt32BE(iv.readUInt32BE(4) ^ ssrc, 4);
// XOR with sequence number
iv.writeUInt16BE(iv.readUInt16BE(14) ^ sequenceNumber, 14);
return iv;
} }
Security Best Practices
Input Validation and Sanitization
class SipSecurityValidator { // Validate SIP URI to prevent injection attacks static validateUri(uri: string): boolean { // Check for valid SIP URI format const sipUriRegex = /^sips?:[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+$/;
if (!sipUriRegex.test(uri)) {
return false;
}
// Check for dangerous characters
const dangerousChars = ['<', '>', '"', "'", ';', '&', '|', '`'];
for (const char of dangerousChars) {
if (uri.includes(char)) {
return false;
}
}
return true;
}
// Validate header values static validateHeader(name: string, value: string): boolean { // Check for CRLF injection if (value.includes('\r') || value.includes('\n')) { return false; }
// Validate specific headers
switch (name.toLowerCase()) {
case 'content-length':
return /^\d+$/.test(value);
case 'max-forwards':
const maxForwards = parseInt(value);
return !isNaN(maxForwards) && maxForwards >= 0 && maxForwards <= 70;
case 'cseq':
return /^\d+\s+[A-Z]+$/.test(value);
default:
return true;
}
}
// Validate SDP to prevent injection static validateSdp(sdp: string): boolean { const lines = sdp.split('\n');
for (const line of lines) {
// Each line should be type=value
if (!line.match(/^[a-z]=.+$/)) {
return false;
}
// Check for dangerous content
if (line.includes('<script>') || line.includes('javascript:')) {
return false;
}
}
return true;
}
// Rate limiting to prevent DoS static checkRateLimit( source: string, maxRequests: number = 10, windowMs: number = 1000 ): boolean { const now = Date.now(); const requests = this.requestCounts.get(source) || { count: 0, resetTime: now + windowMs };
if (now > requests.resetTime) {
// Reset window
requests.count = 1;
requests.resetTime = now + windowMs;
this.requestCounts.set(source, requests);
return true;
}
if (requests.count >= maxRequests) {
return false;
}
requests.count++;
this.requestCounts.set(source, requests);
return true;
}
private static requestCounts = new Map<string, { count: number; resetTime: number }>(); }
Anti-Spoofing Measures
class SipAntiSpoofing { // Validate Via header to detect spoofing static validateVia(via: string, sourceIp: string, sourcePort: number): boolean { // Parse Via header const match = via.match(/SIP/2.0/(\w+)\s+([^;:]+)(?::(\d+))?/);
if (!match) {
return false;
}
const [, transport, host, port] = match;
// Check if received parameter matches source
const receivedMatch = via.match(/;received=([^;]+)/);
if (receivedMatch) {
const received = receivedMatch[1];
if (received !== sourceIp) {
console.warn('Via received parameter mismatch:', received, 'vs', sourceIp);
return false;
}
}
// Check if rport parameter matches source port
const rportMatch = via.match(/;rport(?:=(\d+))?/);
if (rportMatch && rportMatch[1]) {
const rport = parseInt(rportMatch[1]);
if (rport !== sourcePort) {
console.warn('Via rport parameter mismatch:', rport, 'vs', sourcePort);
return false;
}
}
return true;
}
// Add received and rport parameters to Via static addReceivedRport(via: string, sourceIp: string, sourcePort: number): string { let result = via;
// Add received parameter if not present
if (!via.includes('received=')) {
result += `;received=${sourceIp}`;
}
// Add rport parameter value
if (via.includes('rport') && !via.includes('rport=')) {
result = result.replace(/rport/, `rport=${sourcePort}`);
} else if (!via.includes('rport')) {
result += `;rport=${sourcePort}`;
}
return result;
} }
When to Use This Skill
Use sip-authentication-security when building applications that require:
-
User authentication and authorization
-
Secure SIP communications (SIPS/TLS)
-
Protected media streams (SRTP)
-
Registration with authentication
-
Proxy authentication
-
Certificate-based authentication
-
Protection against replay attacks
-
Defense against SIP-specific threats
-
Compliance with security standards
-
Enterprise VoIP security
Best Practices
-
Always use digest authentication - Never send passwords in plaintext
-
Implement nonce expiration - Prevent replay attacks with time-limited nonces
-
Use strong hash algorithms - Prefer SHA-256 over MD5 when possible
-
Validate nonce count (nc) - Detect replay attacks within nonce lifetime
-
Generate cryptographically random values - Use crypto.randomBytes() for nonces, tags
-
Use TLS for signaling - Encrypt SIP messages with TLS (SIPS)
-
Use SRTP for media - Encrypt RTP streams with SRTP
-
Implement mutual TLS - Require client certificates for server authentication
-
Validate all inputs - Sanitize URIs, headers, and SDP content
-
Add received/rport parameters - Prevent Via header spoofing
-
Implement rate limiting - Prevent DoS and brute force attacks
-
Use opaque values - Help detect tampered authentication responses
-
Support stale nonces - Allow clients to retry with fresh nonce
-
Log authentication failures - Monitor for security incidents
-
Rotate master keys regularly - Limit exposure of compromised keys
Common Pitfalls
-
Storing plaintext passwords - Always hash passwords before storage
-
Not validating nonce freshness - Allows replay attacks
-
Weak nonce generation - Predictable nonces compromise security
-
Missing qop parameter - Reduces security, allows easier attacks
-
Not checking nc increments - Misses replay attack attempts
-
Accepting self-signed certificates - Opens door to MITM attacks
-
Using weak cipher suites - Compromises TLS security
-
Not validating certificate chain - Accepts invalid certificates
-
Hardcoded credentials - Security vulnerability in production
-
No rate limiting - Vulnerable to DoS and brute force
-
Missing Content-Length validation - Enables buffer overflow attacks
-
Not sanitizing SDP - Vulnerable to injection attacks
-
Trusting Via headers - Enables IP spoofing attacks
-
Using MD5 in production - Known vulnerabilities, use SHA-256
-
Not implementing SRTP - Exposes media to eavesdropping
Resources
-
RFC 2617 - HTTP Digest Authentication
-
RFC 3261 Section 22 - Security Considerations
-
RFC 3711 - Secure Real-time Transport Protocol (SRTP)
-
RFC 4568 - SDP Security Descriptions
-
RFC 5246 - TLS 1.2
-
RFC 5630 - SIPS URI Scheme
-
RFC 7616 - HTTP Digest Authentication (updated)
-
OWASP SIP Security