1

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.

Nik Hendricks
  • 244
  • 2
  • 6
  • 29
  • 2
    You should [edit] the question to include your updated code as well as a proper SIP trace showing your initial registration request and the server's expected 401 response, as well as the subsequent request/response. I've removed the [tag:asterisk] tag, as the particular server is irrelevant to your question. – miken32 May 23 '23 at 18:22
  • 1
    I have updated my code. and what asterisk returns. – Nik Hendricks May 24 '23 at 05:36
  • 1
    That seems rather sparse. My Asterisk 16.29 server returns a `WWW-Authenticate` header that looks like this: https://pastebin.com/5uxutaZB – miken32 May 24 '23 at 20:52
  • 1
    You say you're using Asterisk 14 but the server response you show is for Asterisk 18, with a bunch of noise added on the version string. In addition, RFC 3261 says "servers **MUST** always send a "qop" parameter in WWW-Authenticate" and it seems unlikely that Asterisk would skip that detail. – miken32 May 24 '23 at 21:08
  • @miken32 ah yes i'm not sure what made me think it was 14 but i confirmed and it is version 18. As for this `qop` parameter in www-authenticate I am not seeing it in my wireshark traces using Zoiper softphone which works perfectly. What could be causing this? – Nik Hendricks May 24 '23 at 21:59
  • Are you using the old chan_sip channel driver by chance? I found an old packet trace from an Asterisk 11 system, and it is missing those additional values. You should definitely be using chan_pjsip at this point, chan_sip has been deprecated for many years now. – miken32 May 24 '23 at 22:12
  • From what I can see in the RFC, your calculations are correct. However, a lot of code is adding additional values to derive the final hash. See [here](https://lists.cs.columbia.edu/pipermail/sip-implementors/2011-April/026747.html), [here](https://github.com/asterisk/asterisk/blob/16/channels/chan_sip.c#L23151), or [here](https://gist.github.com/agranig/2702600) for example. I can't find any official documentation that defines this though. – miken32 May 24 '23 at 22:13
  • @miken32 Thank you very much for all your help so far. Yes, I am using `chan_sip` I intend to upgrade soon I just wanted to ensure support for some older voip phones i have that I'm experimenting with. I will check out the references you gave me thank you. – Nik Hendricks May 24 '23 at 22:36
  • I guess I am just confused. I figured there was a catch-all structure for each of the requests. Is my issue really just lying in the values I'm sending for the nonce and how it is formatted. or is there more variables that come into play? I figured since its SIP it would all be the same regardless except for the auth values. – Nik Hendricks May 24 '23 at 22:42
  • My advice would be to try copying those other code samples by doing the final hash as `${ha1}:${nonce}:${cnonce}:auth:${ha2}` where `cnonce` is a random value you generate. Make sure to return cnonce="whatever" and qop=auth (unquoted) in the response. – miken32 May 24 '23 at 23:12
  • qop is not used here (and not mandatory) and your digest response ${ha1}:${nonce}:${ha2} seems correct. May be your *username* is not *Tim*, but *tim* in lowercase. Worth to try. (or even recreate another user with lowercase to make sure ha1 is also calculated with lowercase on asterisk) – AymericM May 25 '23 at 22:20
  • Thanks everyone for all of the help. I'm trying hard But Even after using other users. Quadruple checking credentials. registration with the following code is unsuccessful. With accounts, I know work. I have been told to look at reference material and I have. But I have also been told that my calculations are correct. I am really at a loss as to what to do here. I would love to award the bounty to someone though. In fact, I would literally be willing to pay for help on getting me started. Seeing as SIP is supposed to be a protocol and all standard-ish I figured my issue would be easy to find. – Nik Hendricks May 27 '23 at 04:19
  • 2
    On my server, the main issue was a wrong CSEQ and CALL-ID headers. I bet the same happened on your asterisk. To avoid attack replay, the nonce can only be used if you follow the guidelines of the protocol: *Call-ID* needs to be the same and *CSEQ* needs to be increased. There was also a tiny \r\n issue. The new code that I proposed is definitely tested and working! I'm thankful you accepted my answer. – AymericM May 30 '23 at 15:01

1 Answers1

1

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:

  • I generate a Call-Id header that is kept over the new REGISTER
  • I use an increasing CSeq for every new REGISTER.
  • Minor modification to use a "domain" instead of "IP" (for my own test purpose)
  • I also removed an extra \r\n at the end of the second REGISTER

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);
});
AymericM
  • 1,645
  • 13
  • 13