SIP认证与安全Skill sip-authentication-security

SIP认证与安全技能专注于VoIP通信中的认证、加密和安全最佳实践,包括HTTP摘要认证、TLS/SIPS加密、SRTP媒体保护,以及防止攻击如重放、DoS和欺骗。关键词:SIP认证, VoIP安全, 加密技术, 身份验证, 网络安全。

身份认证 0 次安装 0 次浏览 更新于 3/25/2026

名称: sip-认证-安全 用户可调用: false 描述: 在实现SIP认证、安全机制和加密时使用。用于保护SIP服务器、客户端或代理。 允许工具:

  • Bash
  • Read

SIP认证与安全

掌握SIP认证机制(HTTP摘要认证)、TLS加密、SIPS以及构建安全VoIP应用的最佳实践。

HTTP摘要认证

挑战-响应流程

客户端                                    服务器
  |                                         |
  | REGISTER(无凭证)                     |
  |---------------------------------------->|
  |                                         |
  |    401未授权                           |
  |    WWW-Authenticate: Digest             |
  |      realm="atlanta.com"                |
  |      nonce="dcd98b7102dd..."            |
  |      algorithm=MD5                      |
  |      qop="auth"                         |
  |<----------------------------------------|
  |                                         |
  | REGISTER(带授权)                     |
  |    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                               |
  |<----------------------------------------|
  |                                         |

摘要认证实现

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 {
  // 生成认证挑战(401/407响应)
  static generateChallenge(realm: string): DigestChallenge {
    return {
      realm,
      nonce: this.generateNonce(),
      algorithm: 'MD5',
      qop: 'auth',
      opaque: this.generateOpaque()
    };
  }

  // 创建WWW-Authenticate或Proxy-Authenticate头
  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;
  }

  // 计算认证响应
  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';

    // 计算A1 = MD5(username:realm:password)
    const a1 = this.hash(
      hashFunc,
      `${params.username}:${params.realm}:${params.password}`
    );

    // 计算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}`);
    }

    // 计算响应
    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;
  }

  // 创建Authorization或Proxy-Authorization头
  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;
  }

  // 验证客户端凭证
  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;
  }

  // 解析Authorization/Proxy-Authorization头
  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');
  }
}

完整认证示例

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;
  }

  // 发送带认证的REGISTER
  async register(server: string): Promise<void> {
    // 首次尝试无凭证
    let response = await this.sendRegister(server);

    if (response.statusCode === 401) {
      // 从WWW-Authenticate头提取挑战
      const challenge = this.parseChallenge(response.headers['www-authenticate']);

      if (!challenge) {
        throw new Error('无效的认证挑战');
      }

      this.realm = challenge.realm;
      this.nonce = challenge.nonce;
      this.opaque = challenge.opaque;

      // 发送带凭证的REGISTER
      response = await this.sendRegister(server, true);
    }

    if (response.statusCode === 200) {
      console.log('注册成功');
    } else {
      throw new Error(`注册失败: ${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
`;
    }

    message += 'Content-Length: 0\r
\r
';

    // 发送消息并获取响应
    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> {
    // 实现取决于传输层
    console.log('发送:', message);
    return { statusCode: 401, headers: {} };
  }
}

服务器端认证

带认证的注册服务器

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;
  }

  // 添加用户到数据库
  addUser(username: string, password: string, domain: string): void {
    this.users.set(username, { username, password, domain });
  }

  // 处理REGISTER请求
  handleRegister(request: SipRequest): SipResponse {
    const authHeader = request.headers['authorization'];

    if (!authHeader) {
      // 无凭证,发送挑战
      return this.sendChallenge();
    }

    // 解析凭证
    const credentials = SipDigestAuth.parseAuthorizationHeader(authHeader);
    if (!credentials) {
      return this.createResponse(400, '错误请求');
    }

    // 验证nonce
    const nonceInfo = this.nonces.get(credentials.nonce);
    if (!nonceInfo) {
      // Nonce过期或无效,发送新挑战
      return this.sendChallenge(true);
    }

    // 检查nonce计数以防止重放攻击
    if (credentials.nc && parseInt(credentials.nc, 16) <= nonceInfo.nc) {
      return this.createResponse(401, '未授权');
    }

    // 获取用户密码
    const user = this.users.get(credentials.username);
    if (!user) {
      return this.createResponse(403, '禁止访问');
    }

    // 验证凭证
    const valid = SipDigestAuth.verifyCredentials(
      credentials,
      user.password,
      'REGISTER'
    );

    if (!valid) {
      return this.createResponse(403, '禁止访问');
    }

    // 更新nonce计数
    if (credentials.nc) {
      nonceInfo.nc = parseInt(credentials.nc, 16);
    }

    // 注册联系人
    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;

    // 存储nonce
    this.nonces.set(challenge.nonce, {
      nonce: challenge.nonce,
      timestamp: Date.now(),
      nc: 0
    });

    const response = this.createResponse(401, '未授权');
    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);

    // 设置过期计时器
    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
    };
  }

  // 清理过期的nonces
  cleanupNonces(): void {
    const now = Date.now();
    const maxAge = 300000; // 5分钟

    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实现

安全SIP(SIPS)设置

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),
      // 要求客户端证书以进行相互TLS
      requestCert: true,
      rejectUnauthorized: this.config.rejectUnauthorized !== false,
      // 使用强TLS版本
      minVersion: (this.config.minVersion as any) || 'TLSv1.2',
      // 使用安全密码套件
      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('安全连接已建立');

    // 验证客户端证书
    if (socket.authorized) {
      console.log('客户端证书已验证');
      const cert = socket.getPeerCertificate();
      console.log('客户端CN:', cert.subject.CN);
    } else {
      console.log('客户端证书未授权:', socket.authorizationError);
      socket.destroy();
      return;
    }

    socket.on('data', (data) => {
      this.handleData(socket, data);
    });

    socket.on('error', (error) => {
      console.error('套接字错误:', error);
    });

    socket.on('close', () => {
      console.log('连接已关闭');
    });
  }

  private handleData(socket: tls.TLSSocket, data: Buffer): void {
    const message = data.toString();
    console.log('收到:', message);

    // 处理SIP消息
    // 发送响应
  }

  listen(port: number, host: string = '0.0.0.0'): void {
    this.server.listen(port, host, () => {
      console.log(`SIP TLS服务器监听于 ${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('连接到服务器,证书已验证');
          resolve(socket);
        } else {
          console.error('证书验证失败:', socket.authorizationError);
          socket.destroy();
          reject(new Error('证书验证失败'));
        }
      });

      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('收到:', data.toString());
    });
  }
}

// 使用示例
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和媒体安全

SDP中的SRTP密钥交换

v=0
o=alice 2890844526 2890844526 IN IP4 pc33.atlanta.com
s=安全会话
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实现

import crypto from 'crypto';

interface SrtpParams {
  cryptoSuite: string;
  keyParams: string;
  sessionParams?: string;
}

class SrtpCrypto {
  // 从SDP解析crypto属性
  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
    };
  }

  // 为SDP生成crypto属性
  static generateCryptoAttribute(tag: number = 1): string {
    const cryptoSuite = 'AES_CM_128_HMAC_SHA1_80';
    const masterKey = crypto.randomBytes(16); // 128位
    const masterSalt = crypto.randomBytes(14); // 112位

    // 连接密钥和盐
    const keyMaterial = Buffer.concat([masterKey, masterSalt]);
    const keyParams = keyMaterial.toString('base64');

    // 会话参数
    const lifetime = '2^20'; // 2^20数据包
    const mki = '1:32';      // MKI值:长度

    return `crypto:${tag} ${cryptoSuite} inline:${keyParams}|${lifetime}|${mki}`;
  }

  // 从主密钥派生会话密钥
  static deriveSessionKeys(
    masterKey: Buffer,
    masterSalt: Buffer,
    index: number
  ): {
    encryptionKey: Buffer;
    authKey: Buffer;
    saltingKey: Buffer;
  } {
    // 根据RFC 3711进行密钥派生
    const kdr = 0; // 密钥派生率(0 = 永不重新密钥)

    // 计算key_id
    const r = index / (2 ** kdr);

    // 派生加密密钥
    const encryptionKey = this.deriveKey(masterKey, masterSalt, 0x00, r);

    // 派生认证密钥
    const authKey = this.deriveKey(masterKey, masterSalt, 0x01, r);

    // 派生加盐密钥
    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)))
    // 简化实现
    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;
  }

  // 加密RTP数据包
  static encryptRtp(
    packet: Buffer,
    encryptionKey: Buffer,
    saltingKey: Buffer,
    ssrc: number,
    sequenceNumber: number
  ): Buffer {
    // 提取RTP头(前12字节)
    const header = packet.slice(0, 12);

    // 提取负载
    const payload = packet.slice(12);

    // 从加盐密钥和数据包索引生成IV
    const iv = this.generateIv(saltingKey, ssrc, sequenceNumber);

    // 加密负载
    const cipher = crypto.createCipheriv('aes-128-ctr', encryptionKey, iv);
    const encryptedPayload = Buffer.concat([
      cipher.update(payload),
      cipher.final()
    ]);

    // 连接头和加密负载
    return Buffer.concat([header, encryptedPayload]);
  }

  // 生成认证标签
  static generateAuthTag(
    packet: Buffer,
    authKey: Buffer
  ): Buffer {
    const hmac = crypto.createHmac('sha1', authKey);
    hmac.update(packet);
    const tag = hmac.digest();

    // 对AES_CM_128_HMAC_SHA1_80使用前10字节
    return tag.slice(0, 10);
  }

  private static generateIv(
    saltingKey: Buffer,
    ssrc: number,
    sequenceNumber: number
  ): Buffer {
    const iv = Buffer.alloc(16);

    // 复制加盐密钥
    saltingKey.copy(iv);

    // XOR with SSRC
    iv.writeUInt32BE(iv.readUInt32BE(4) ^ ssrc, 4);

    // XOR with 序列号
    iv.writeUInt16BE(iv.readUInt16BE(14) ^ sequenceNumber, 14);

    return iv;
  }
}

安全最佳实践

输入验证和清理

class SipSecurityValidator {
  // 验证SIP URI以防止注入攻击
  static validateUri(uri: string): boolean {
    // 检查有效的SIP URI格式
    const sipUriRegex = /^sips?:[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+$/;

    if (!sipUriRegex.test(uri)) {
      return false;
    }

    // 检查危险字符
    const dangerousChars = ['<', '>', '"', "'", ';', '&', '|', '`'];
    for (const char of dangerousChars) {
      if (uri.includes(char)) {
        return false;
      }
    }

    return true;
  }

  // 验证头值
  static validateHeader(name: string, value: string): boolean {
    // 检查CRLF注入
    if (value.includes('\r') || value.includes('
')) {
      return false;
    }

    // 验证特定头
    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;
    }
  }

  // 验证SDP以防止注入
  static validateSdp(sdp: string): boolean {
    const lines = sdp.split('
');

    for (const line of lines) {
      // 每行应为type=value
      if (!line.match(/^[a-z]=.+$/)) {
        return false;
      }

      // 检查危险内容
      if (line.includes('<script>') || line.includes('javascript:')) {
        return false;
      }
    }

    return true;
  }

  // 速率限制以防止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) {
      // 重置窗口
      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 }>();
}

反欺骗措施

class SipAntiSpoofing {
  // 验证Via头以检测欺骗
  static validateVia(via: string, sourceIp: string, sourcePort: number): boolean {
    // 解析Via头
    const match = via.match(/SIP\/2.0\/(\w+)\s+([^;:]+)(?::(\d+))?/);

    if (!match) {
      return false;
    }

    const [, transport, host, port] = match;

    // 检查接收参数是否匹配源
    const receivedMatch = via.match(/;received=([^;]+)/);
    if (receivedMatch) {
      const received = receivedMatch[1];
      if (received !== sourceIp) {
        console.warn('Via接收参数不匹配:', received, '对比', sourceIp);
        return false;
      }
    }

    // 检查rport参数是否匹配源端口
    const rportMatch = via.match(/;rport(?:=(\d+))?/);
    if (rportMatch && rportMatch[1]) {
      const rport = parseInt(rportMatch[1]);
      if (rport !== sourcePort) {
        console.warn('Via rport参数不匹配:', rport, '对比', sourcePort);
        return false;
      }
    }

    return true;
  }

  // 添加接收和rport参数到Via
  static addReceivedRport(via: string, sourceIp: string, sourcePort: number): string {
    let result = via;

    // 添加接收参数(如果不存在)
    if (!via.includes('received=')) {
      result += `;received=${sourceIp}`;
    }

    // 添加rport参数值
    if (via.includes('rport') && !via.includes('rport=')) {
      result = result.replace(/rport/, `rport=${sourcePort}`);
    } else if (!via.includes('rport')) {
      result += `;rport=${sourcePort}`;
    }

    return result;
  }
}

何时使用此技能

在构建需要以下功能的应用时使用sip-认证-安全:

  • 用户认证和授权
  • 安全SIP通信(SIPS/TLS)
  • 受保护的媒体流(SRTP)
  • 带认证的注册
  • 代理认证
  • 基于证书的认证
  • 防止重放攻击
  • 防御SIP特定威胁
  • 符合安全标准
  • 企业VoIP安全

最佳实践

  1. 始终使用摘要认证 - 切勿以明文发送密码
  2. 实现nonce过期 - 使用时间限制的nonce防止重放攻击
  3. 使用强哈希算法 - 可能时优先使用SHA-256而非MD5
  4. 验证nonce计数(nc) - 检测nonce生命周期内的重放攻击
  5. 生成密码学随机值 - 对nonce、标签等使用crypto.randomBytes()
  6. 使用TLS进行信令 - 使用TLS(SIPS)加密SIP消息
  7. 使用SRTP进行媒体 - 使用SRTP加密RTP流
  8. 实现相互TLS - 要求客户端证书以进行服务器认证
  9. 验证所有输入 - 清理URI、头和SDP内容
  10. 添加接收/rport参数 - 防止Via头欺骗
  11. 实现速率限制 - 防止DoS和暴力攻击
  12. 使用不透明值 - 帮助检测篡改的认证响应
  13. 支持过时nonce - 允许客户端使用新nonce重试
  14. 记录认证失败 - 监控安全事件
  15. 定期轮换主密钥 - 限制受损密钥的暴露

常见陷阱

  1. 存储明文密码 - 存储前始终哈希密码
  2. 未验证nonce新鲜度 - 允许重放攻击
  3. 弱nonce生成 - 可预测的nonce危及安全
  4. 缺少qop参数 - 降低安全性,允许更易攻击
  5. 未检查nc递增 - 错过重放攻击尝试
  6. 接受自签名证书 - 为中间人攻击打开大门
  7. 使用弱密码套件 - 损害TLS安全
  8. 未验证证书链 - 接受无效证书
  9. 硬编码凭证 - 生产中的安全漏洞
  10. 无速率限制 - 易受DoS和暴力攻击
  11. 缺少Content-Length验证 - 启用缓冲区溢出攻击
  12. 未清理SDP - 易受注入攻击
  13. 信任Via头 - 启用IP欺骗攻击
  14. 在生产中使用MD5 - 已知漏洞,使用SHA-256
  15. 未实现SRTP - 暴露媒体于窃听

资源