--- name: sip-authentication-security user-invocable: false description: Use when implementing SIP authentication, security mechanisms, and encryption. Use when securing SIP servers, clients, or proxies. allowed-tools: - Bash - Read --- # 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 ```typescript 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 ```typescript 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 { // 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 { 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: \r From: ;tag=${this.generateTag()}\r Call-ID: ${this.generateCallId()}\r CSeq: 1 REGISTER\r Contact: \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 { // Implementation depends on transport console.log('Sending:', message); return { statusCode: 401, headers: {} }; } } ``` ## Server-Side Authentication ### Registration Server with Authentication ```typescript interface UserCredentials { username: string; password: string; domain: string; } class SipRegistrar { private users: Map = new Map(); private registrations: Map = new Map(); private nonces: Map = 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 ```typescript 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 { 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 { 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 ```typescript 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 ```typescript 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('