node.jsprotocolssipvoip

Node.js Asterisk authentications sip UDP headers


I am trying to learn more about SIP and trying to implement small parts of the protocol in node.js with UDP

so far I have this

const dgram = require('dgram');
const crypto = require('crypto');

const asteriskIP = 'ASTERISK_IP';
const asteriskPort = 6111;
const clientIP = '192.168.1.2';
const clientPort = 6111;
const username = 'USERNAME';
const password = 'PASSWORD';

// Create a UDP socket
const socket = dgram.createSocket('udp4');

// Generate a random branch identifier for the Via header
const generateBranch = () => {
  const branchId = Math.floor(Math.random() * 100000000);
  return `z9hG4bK${branchId}`;
};

// Generate Digest response
function generateDigestResponse(username, password, realm, nonce, method, uri) {
  const ha1 = crypto.createHash('md5')
    .update(`${username}:${realm}:${password}`)
    .digest('hex');

  const ha2 = crypto.createHash('md5')
    .update(`${method}:${uri}`)
    .digest('hex');

  const response = crypto.createHash('md5')
    .update(`${ha1}:${nonce}:${ha2}`)
    .digest('hex');

  return response;
}

// SIP REGISTER request
const generateRegisterRequest = (branch, withAuth = false, realm = '', nonce = '') => {
  let request = `REGISTER sip:${asteriskIP}:${asteriskPort} SIP/2.0\r\n` +
    `Via: SIP/2.0/UDP ${clientIP}:${clientPort};branch=${branch}\r\n` +
    `From: <sip:${username}@${asteriskIP}>;tag=${branch}\r\n` +
    `To: <sip:${username}@${asteriskIP}>\r\n` +
    `Call-ID: ${branch}@${clientIP}\r\n` +
    `CSeq: 1 REGISTER\r\n` +
    `Contact: <sip:${username}@${clientIP}:${clientPort}>\r\n` +
    'Max-Forwards: 70\r\n' +
    'Expires: 3600\r\n' +
    'User-Agent: Node.js SIP Library\r\n';

  if (withAuth && realm && nonce) {
    const digestResponse = generateDigestResponse(username, password, realm, nonce, 'REGISTER', `sip:${asteriskIP}:${asteriskPort}`);
    request += 'Authorization: Digest ' +
      `username="${username}", realm="${realm}", ` +
      `nonce="${nonce}", uri="sip:${asteriskIP}:${asteriskPort}", ` +
      `response="${digestResponse}"\r\n`;
  }

  request += 'Content-Length: 0\r\n\r\n';

  return request;
};

// Send the REGISTER request
const sendRegisterRequest = (request) => {
  socket.send(request, 0, request.length, asteriskPort, asteriskIP, (error) => {
    if (error) {
      console.error('Error sending UDP packet:', error);
    } else {
      console.log('REGISTER request sent successfully.');
    }
  });
};

let realm = '';
let nonce = '';

// Listen for incoming responses
socket.on('message', (message) => {
  const response = message.toString();
  console.log('Received response:', response);

  if (response.startsWith('SIP/2.0 200 OK')) {
    console.log('Registration successful.');
    // Do further processing or initiate calls here
  } else if (response.startsWith('SIP/2.0 401 Unauthorized')) {
    const authenticateHeader = response.match(/WWW-Authenticate:.*realm="([^"]+)".*nonce="([^"]+)"/i);
    if (authenticateHeader) {
      realm = authenticateHeader[1];
      nonce = authenticateHeader[2];
      console.log('Received realm:', realm);
      console.log('Received nonce:', nonce);

      // Generate Digest response and proceed with registration
      const branch = generateBranch();
      const registerRequestWithAuth = generateRegisterRequest(branch, true, realm, nonce);

      console.log('Sending REGISTER request with authentication:');
      console.log(registerRequestWithAuth);
      // Send the REGISTER request with authentication
      sendRegisterRequest(registerRequestWithAuth+ '\r\n');
    }
  }
});

// Bind the socket to the client's port and IP
socket.bind(clientPort, clientIP, () => {
  console.log('Socket bound successfully.');

  // Generate branch identifier for the initial REGISTER request
  const branch = generateBranch();
  const registerRequest = generateRegisterRequest(branch);

  // Send the initial REGISTER request
  sendRegisterRequest(registerRequest);
});

I have turned on SIP debugging in asterisk 18 and this is what I see.

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK2049260;received=72.172.213.173;rport=6111
From: <sip:Tim@64.227.16.15>;tag=z9hG4bK2049260
To: <sip:Tim@64.227.16.15>;tag=as12883ca4
Call-ID: z9hG4bK2049260@192.168.1.2
CSeq: 1 REGISTER
Server: Asterisk PBX 18.14.0~dfsg+~cs6.12.40431414-1
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO, PUBLISH, MESSAGE
Supported: replaces
WWW-Authenticate: Digest algorithm=MD5, realm="asterisk", nonce="5b4af6d3"
Content-Length: 0

REGISTER sip:ASTERISK_IP:6111 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK99247361
From: <sip:Tim@ASTERISK_IP>;tag=z9hG4bK99247361
To: <sip:Tim@ASTERISK_IP>
Call-ID: z9hG4bK99247361@192.168.1.2
CSeq: 1 REGISTER
Contact: <sip:Tim@192.168.1.2:6111>
Max-Forwards: 70
Expires: 3600
User-Agent: Node.js SIP Library
Authorization: Digest username="Tim", realm="asterisk", nonce="5b4af6d3", uri="sip:ASTERISK_IP:6111", response="2e0cbc55739537d46ff0c0ff862ae28a"
Content-Length: 0

SIP/2.0 401 Unauthorized
Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK99247361;received=72.172.213.173;rport=6111
From: <sip:Tim@ASTERISK_IP>;tag=z9hG4bK99247361
To: <sip:Tim@ASTERISK_IP>;tag=as75c4d7b6
Call-ID: z9hG4bK99247361@192.168.1.2
CSeq: 1 REGISTER
Server: Asterisk PBX 18.14.0~dfsg+~cs6.12.40431414-1
Allow: INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, SUBSCRIBE, NOTIFY, INFO, PUBLISH, MESSAGE
Supported: replaces
WWW-Authenticate: Digest algorithm=MD5, realm="asterisk", nonce="1fd88a71"
Content-Length: 0

But I am not getting 200 OK from asterisk at all. I am wondering if I am calculating the nonce incorrectly. I know the code is a bit messy but this is just a quick test first. Am I missing any headers? does asterisk need something more? Any help or push in the right direction would be greatly appreciated. Thank you.


Solution

  • Authorization is a header and headers needs to all appear before the occurence of \r\n\r\n (empty line)

    A common place would be to put the Authorization just before the Content-Length header. With only \r\n before and after.

    After the \r\n\r\n, you will have a body (if content-length indicate one), or a new SIP message.

    UPDATE: Here is rfc3261 Section 20.7 to make it clear that Authorization is a SIP header:

    20.7 Authorization

    The Authorization header field contains authentication credentials of a UA. Section 22.2 overviews the use of the Authorization header field, and Section 22.4 describes the syntax and semantics when used with HTTP authentication.

    Your second REGISTER needs to be like this:

    REGISTER sip:ASTERISKIP:ASTERISKPORT SIP/2.0
    Via: SIP/2.0/UDP 192.168.1.2:6111;branch=z9hG4bK30130523
    From: <sip:USERNAME@ASTERISKIP>;tag=z9hG4bK30130523
    To: <sip:USERNAME@ASTERISKIP>
    Call-ID: z9hG4bK30130523@192.168.1.2
    CSeq: 1 REGISTER
    Contact: <sip:Tim@192.168.1.2:6111>
    Max-Forwards: 70
    Expires: 3600
    User-Agent: Node.js SIP Library
    Authorization: Digest username="Tim", realm="asterisk", nonce="120ef084", uri="sip:ASTERISKIP:ASTERISKPORT", response="e6a25e09e1bb2099436cf10526d955e0"
    Content-Length: 0
    

    UPDATE 2:

    You should really define a lowercase username on both your server (when you create the password) AND on the client, when you configure the username. It is pretty common to have case issue on HTTP or SIP software. A username is usually case-insensitive, so using Tim will produce error if asterisk consider it as lowercase.

    In order to validate your md5 response, the easiest is to calculate the string with the tool md5sum:

    $> echo -n "Tim:asterisk:secret" | md5sum
    d86544eb768e7936519f727599d51fbb
    
    $> echo -n "REGISTER:sip:ASTERISK_IP:6111" | md5sum
    0a1fc837871b52dfe5c7d9a009079265
    
    $> echo -n "d86544eb768e7936519f727599d51fbb:5b4af6d3:0a1fc837871b52dfe5c7d9a009079265" | md5sum
    504804156ca15812da67ea5439a9ea00
    

    Then, validate if this is the result of your code! I can't do it because have you hidden your real values.

    You can try with "Tim" or "tim", to see the difference.

    UPDATE 3:

    I have tested your code and fixed a few things:

    When this is not done, the server may detect this as an attack. My own server (a kamailio) was rejecting the request with a "nonce expired" information.

    This code is working for sip.antisip.com and "stackoverflow" user with password "WuZkA6T@sQ8W". I will remove this user very soon from my service. But you can validate that they work.

    const dgram = require("dgram");
    const crypto = require("crypto");
    
    const asteriskDOMAIN = "sip.antisip.com";
    const asteriskIP = "94.23.17.185";
    const asteriskPort = 5060;
    const clientIP = "192.168.1.9";
    const clientPort = 6111;
    const username = "stackoverflow";
    const password = "WuZkA6T@sQ8W";
    let callId;
    let cseq = 1;
    
    // Create a UDP socket
    const socket = dgram.createSocket("udp4");
    
    // Generate a random branch identifier for the Via header
    const generateBranch = () => {
      const branchId = Math.floor(Math.random() * 10000000000000);
      return `z9hG4bK${branchId}X2`;
    };
    
    const generateCallid = () => {
      const branchId = Math.floor(Math.random() * 10000000000000);
      return `${branchId}`;
    };
    
    // Generate Digest response
    function generateDigestResponse(username, password, realm, nonce, method, uri) {
      const ha1 = crypto.createHash("md5")
        .update(`${username}:${realm}:${password}`)
        .digest("hex");
    
      const ha2 = crypto.createHash("md5")
        .update(`${method}:${uri}`)
        .digest("hex");
    
      const response = crypto.createHash("md5")
        .update(`${ha1}:${nonce}:${ha2}`)
        .digest("hex");
      return response;
    }
    
    // SIP REGISTER request
    const generateRegisterRequest = (branch, withAuth = false, realm = "", nonce = "") => {
      let request = `REGISTER sip:${asteriskDOMAIN}:${asteriskPort} SIP/2.0\r\n`
        + `Via: SIP/2.0/UDP ${clientIP}:${clientPort};branch=${branch}\r\n`
        + `From: <sip:${username}@${asteriskDOMAIN}>;tag=${branch}\r\n`
        + `To: <sip:${username}@${asteriskDOMAIN}>\r\n`
        + `Call-ID: ${callId}@${clientIP}\r\n`
        + `CSeq: ${cseq} REGISTER\r\n`
        + `Contact: <sip:${username}@${clientIP}:${clientPort}>\r\n`
        + "Max-Forwards: 70\r\n"
        + "Expires: 3600\r\n"
        + "User-Agent: Node.js SIP Library\r\n";
    
      cseq += 1;
    
      if (withAuth && realm && nonce) {
        const digestResponse = generateDigestResponse(username, password, realm, nonce, "REGISTER", `sip:${asteriskDOMAIN}:${asteriskPort}`);
        request += "Authorization: Digest "
          + `username="${username}", realm="${realm}", `
          + `nonce="${nonce}", uri="sip:${asteriskDOMAIN}:${asteriskPort}", `
          + `response="${digestResponse}"\r\n`;
      }
    
      request += "Content-Length: 0\r\n\r\n";
    
      return request;
    };
    
    // Send the REGISTER request
    const sendRegisterRequest = (request) => {
      socket.send(request, 0, request.length, asteriskPort, asteriskIP, (error) => {
        if (error) {
          console.error("Error sending UDP packet:", error);
        } else {
          console.log("REGISTER request sent successfully.");
        }
      });
    };
    
    let realm = "";
    let nonce = "";
    
    // Listen for incoming responses
    socket.on("message", (message) => {
      const response = message.toString();
      console.log("Received response:", response);
    
      if (response.startsWith("SIP/2.0 200 OK")) {
        console.log("Registration successful.");
        // Do further processing or initiate calls here
      } else if (response.startsWith("SIP/2.0 401 Unauthorized")) {
        const authenticateHeader = response.match(/WWW-Authenticate:.*realm="([^"]+)".*nonce="([^"]+)"/i);
        if (authenticateHeader) {
          realm = authenticateHeader[1];
          nonce = authenticateHeader[2];
          console.log("Received realm:", realm);
          console.log("Received nonce:", nonce);
    
          // Generate Digest response and proceed with registration
          const branch = generateBranch();
          const registerRequestWithAuth = generateRegisterRequest(branch, true, realm, nonce);
    
          console.log("Sending REGISTER request with authentication:");
          console.log(registerRequestWithAuth);
          // Send the REGISTER request with authentication
          sendRegisterRequest(`${registerRequestWithAuth}`);
        }
      }
    });
    
    // Bind the socket to the client's port and IP
    socket.bind(clientPort, clientIP, () => {
      console.log("Socket bound successfully.");
    
      // Generate branch identifier for the initial REGISTER request
      const branch = generateBranch();
      callId = generateCallid();
      const registerRequest = generateRegisterRequest(branch);
    
      // Send the initial REGISTER request
      sendRegisterRequest(registerRequest);
    });