asterisk: Implement and test SIP Digest Authorization

Related: SYS#6782
Change-Id: Ib469f1906927a3f246876040086ff115fbf4c032
diff --git a/library/SIP_Templates.ttcn b/library/SIP_Templates.ttcn
index fc8e23f..7cb9d68 100644
--- a/library/SIP_Templates.ttcn
+++ b/library/SIP_Templates.ttcn
@@ -1,7 +1,11 @@
 module SIP_Templates {
 
 import from SIPmsg_Types all;
+import from TCCConversion_Functions all;
+import from TCCOpenSecurity_Functions all;
+import from Native_Functions all;
 import from Osmocom_Types all;
+import from Misc_Helpers all;
 
 /* wrapper type to encapsulate the Addr_Union + parameter list used in From, To. ... */
 type record SipAddr {
@@ -11,6 +15,26 @@
 
 const charstring c_SIP_VERSION := "SIP/2.0";
 
+template (value) GenericParam ts_Param(template (value) charstring id,
+				       template (omit) charstring paramValue := omit) := {
+	id := id,
+	paramValue := paramValue
+}
+template (present) GenericParam tr_Param(template (present) charstring id := ?,
+					 template charstring paramValue := *) := {
+	id := id,
+	paramValue := paramValue
+}
+function f_ts_Param_omit(template (value) charstring id,
+			 template (omit) charstring paramValue := omit)
+	return template (omit) GenericParam
+{
+	if (istemplatekind(paramValue, "omit")) {
+		return omit;
+	}
+	return ts_Param(id, paramValue);
+}
+
 template (value) SipUrl ts_SipUrl(template (value) HostPort host_port,
 				  template (omit) UserInfo user_info := omit) := {
 	scheme := "sip",
@@ -31,6 +55,49 @@
 template (value) SipUrl ts_SipUrlHost(template (value) charstring host)
 	:= ts_SipUrl(ts_HostPort(host));
 
+template (value) Credentials ts_Credentials_DigestResponse(template (value) CommaParam_List digestResponse) := {
+	digestResponse := digestResponse
+}
+
+template (value) Credentials ts_Credentials_DigestResponseMD5(
+			template (value) charstring username,
+			template (value) charstring realm,
+			template (value) charstring nonce,
+			template (value) charstring uri,
+			template (value) charstring response,
+			template (value) charstring opaque,
+			template (value) charstring algorithm := "MD5",
+			template (value) charstring qop := "auth",
+			template (omit) charstring cnonce := omit,
+			template (omit) charstring nc := omit
+			) := {
+	digestResponse := {
+		// Already added by digestResponse automatically:
+		//ts_Param("Digest", omit),
+		ts_Param("username", f_sip_str_quote(username)),
+		ts_Param("realm", f_sip_str_quote(realm)),
+		ts_Param("nonce", f_sip_str_quote(nonce)),
+		ts_Param("uri", f_sip_str_quote(uri)),
+		ts_Param("response", f_sip_str_quote(response)),
+		ts_Param("opaque", f_sip_str_quote(opaque)),
+		ts_Param("algorithm", algorithm),
+		ts_Param("qop", qop),
+		// FIXME: If "omit" is passed, these below end up in;
+		// "Dynamic test case error: Performing a valueof or send operation on a non-specific template of type @SIPmsg_Types.GenericParam"
+		f_ts_Param_omit("cnonce", f_sip_str_quote(cnonce)),
+		f_ts_Param_omit("nc", nc)
+	}
+}
+
+template (value) Credentials ts_Credentials_OtherAuth(template (value) OtherAuth otherResponse) := {
+	otherResponse := otherResponse
+}
+
+template (value) Authorization ts_Authorization(template (value) Credentials body) := {
+	fieldName := AUTHORIZATION_E,
+	body := body
+}
+
 // [20.10]
 template (present) NameAddr tr_NameAddr(template (present) SipUrl addrSpec := ?,
 				      template charstring displayName := *) := {
@@ -262,7 +329,53 @@
 			viaParams := viaParams
 		}
 	}
-	}
+}
+
+template (present) OtherAuth
+tr_OtherAuth(template (present) charstring authScheme := ?,
+	     template (present) CommaParam_List authParams := ?) := {
+	authScheme := authScheme,
+	authParams := authParams
+}
+
+template (value) OtherAuth
+ts_OtherAuth(template (value) charstring authScheme,
+	     template (value) CommaParam_List authParams) := {
+	authScheme := authScheme,
+	authParams := authParams
+}
+
+template (present) Challenge
+tr_Challenge_digestCln(template (present) CommaParam_List digestCln := ?) := {
+	digestCln := digestCln
+}
+
+template (value) Challenge
+ts_Challenge_digestCln(template (value) CommaParam_List digestCln) := {
+	digestCln := digestCln
+}
+
+template (present) Challenge
+tr_Challenge_otherChallenge(template (present) OtherAuth otherChallenge := ?) := {
+	otherChallenge := otherChallenge
+}
+
+template (value) Challenge
+ts_Challenge_otherChallenge(template (value) OtherAuth otherChallenge) := {
+	otherChallenge := otherChallenge
+}
+
+template (present) WwwAuthenticate
+tr_WwwAuthenticate(template (present) Challenge_list challenge := ?) := {
+	fieldName := WWW_AUTHENTICATE_E,
+	challenge := challenge
+}
+
+template (value) WwwAuthenticate
+ts_WwwAuthenticate(template (value) Challenge_list challenge) := {
+	fieldName := WWW_AUTHENTICATE_E,
+	challenge := challenge
+}
 
 template (value) MessageHeader ts_SIP_msgHeader_empty := c_SIP_msgHeader_empty;
 template (value) MessageHeader
@@ -274,6 +387,7 @@
 		template (value) integer seq_nr,
 		template (value) Via via,
 		template (omit) ContentType content_type := omit,
+		template (omit)Authorization authorization := omit,
 		template (value) Method_List allow_methods := c_SIP_defaultMethods,
 		template (omit) Expires expires := omit
 	) modifies ts_SIP_msgHeader_empty := {
@@ -281,6 +395,7 @@
 		fieldName := ALLOW_E,
 		methods := allow_methods
 	},
+	authorization := authorization,
 	callId := {
 		fieldName := CALL_ID_E,
 		callid := call_id
@@ -337,7 +452,8 @@
 		template ContentType content_type := *,
 		template integer seq_nr := ?,
 		template Method_List allow_methods := *,
-		template Expires expires := *
+		template Expires expires := *,
+		template WwwAuthenticate wwwAuthenticate := *
 		) modifies t_SIP_msgHeader_any := {
 	allow := tr_AllowMethods(allow_methods),
 	callId := {
@@ -363,7 +479,8 @@
 		toParams := to_addr.params
 	},
 	userAgent := *,
-	via := via
+	via := via,
+	wwwAuthenticate := wwwAuthenticate
 }
 
 
@@ -376,11 +493,13 @@
 		integer seq_nr,
 		template (omit) Contact contact,
 		template (omit) Expires expires,
+		template (omit) Authorization authorization := omit,
 		template (omit) charstring body := omit) := {
 	requestLine := ts_SIP_ReqLine(REGISTER_E, sip_url_host_port),
 	msgHeader := ts_SIP_msgh_std(call_id, from_addr, to_addr, contact,
 				     "REGISTER", seq_nr, via,
 				     f_ContentTypeOrOmit(ts_CT_SDP, body),
+				     authorization := authorization,
 				     expires := expires),
 	messageBody := body,
 	payload := omit
@@ -511,15 +630,16 @@
 tr_SIP_Response(template CallidString call_id,
 		template SipAddr from_addr,
 		template SipAddr to_addr,
+		template (present) Via via := tr_Via_from(?),
 		template Contact contact,
 		template charstring method,
 		template integer status_code,
 		template integer seq_nr := ?,
 		template charstring reason := ?,
-		template charstring body := ?) := {
+		template charstring body := *) := {
 	statusLine := tr_SIP_StatusLine(status_code, reason),
 	msgHeader := tr_SIP_msgh_std(call_id, from_addr, to_addr, contact,
-				     tr_Via_from(tr_HostPort(from_addr.addr.nameAddr.addrSpec.hostPort)),
+				     via,
 				     method, *, seq_nr),
 	messageBody := body,
 	payload := omit
@@ -533,6 +653,7 @@
 	template SipAddr to_addr,
 	template (present) Via via := tr_Via_from(?),
 	template Contact contact := *,
+	template (present) WwwAuthenticate wwwAuthenticate := ?,
 	template integer seq_nr := ?,
 	template charstring method := "REGISTER",
 	template integer status_code := 401,
@@ -541,11 +662,333 @@
 	statusLine := tr_SIP_StatusLine(status_code, reason),
 	msgHeader := tr_SIP_msgh_std(call_id, from_addr, to_addr, contact,
 				     via,
-				     method, *, seq_nr),
+				     method, *, seq_nr,
+				     wwwAuthenticate := wwwAuthenticate),
 	messageBody := body,
 	payload := omit
 }
 
+function f_sip_param_find(GenericParam_List li,
+			  template (present) charstring id := ?)
+return template (omit) GenericParam {
+	var integer i;
+
+	for (i := 0; i < lengthof(li); i := i + 1) {
+		if (not ispresent(li[i])) {
+			continue;
+		}
+		if (match(li[i].id, id)) {
+			return li[i];
+		}
+	}
+	return omit;
+}
+
+function f_sip_param_find_or_fail(GenericParam_List li,
+				  template (present) charstring id := ?)
+return GenericParam {
+	var template (omit) GenericParam parameter;
+	parameter := f_sip_param_find(li, id);
+	if (istemplatekind(parameter, "omit")) {
+		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
+			log2str("Param ", id, " not found in ", li));
+	}
+	return valueof(parameter);
+}
+
+function f_sip_param_get_value(GenericParam_List li,
+			       template (present) charstring id := ?)
+return template (omit) charstring {
+	var template (omit) GenericParam parameter;
+	parameter := f_sip_param_find(li, id);
+	if (istemplatekind(parameter, "omit")) {
+		return omit;
+	}
+	return parameter.paramValue;
+}
+
+function f_sip_param_get_value_or_fail(GenericParam_List li,
+					       template (present) charstring id := ?)
+return template (omit) charstring {
+	var GenericParam parameter;
+	parameter := f_sip_param_find_or_fail(li, id);
+	return parameter.paramValue;
+}
+
+function f_sip_param_get_value_present_or_fail(GenericParam_List li,
+				       template (present) charstring id := ?)
+return charstring {
+	var GenericParam parameter;
+	parameter := f_sip_param_find_or_fail(li, id);
+	if (not ispresent(parameter.paramValue)) {
+		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
+					log2str("Param ", id, " value not present in ", li));
+	}
+	return parameter.paramValue;
+}
+
+function f_sip_param_match_value(GenericParam_List li,
+				 template (present) charstring id := ?,
+				 template charstring exp_paramValue := *)
+return boolean {
+	var template (omit) charstring val;
+	val := f_sip_param_get_value_or_fail(li, id);
+	if (istemplatekind(val, "omit")) {
+		return istemplatekind(val, "omit") or istemplatekind(val, "*");
+	}
+	return match(valueof(val), exp_paramValue);
+}
+
+function f_sip_param_match_value_or_fail(GenericParam_List li,
+					 template (present) charstring id := ?,
+					 template charstring exp_paramValue := *)
+{
+	var template (omit) charstring val := f_sip_param_get_value_or_fail(li, id);
+	if (istemplatekind(val, "omit")) {
+		if (istemplatekind(val, "omit") or istemplatekind(val, "*")) {
+			return;
+		} else {
+			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
+						log2str("Param ", id, " match failed: val ", val,
+							" vs exp ", exp_paramValue));
+		}
+	}
+	if (not match(valueof(val), exp_paramValue)) {
+		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
+					log2str("Param ", id, " match failed: val ", val,
+						" vs exp ", exp_paramValue));
+	}
+}
+
+function f_sip_param_remove(template (omit) GenericParam_List li_tpl, charstring id)
+return GenericParam_List {
+	var integer i;
+	var GenericParam_List li;
+	var GenericParam_List new_li := {};
+
+	if (istemplatekind(li_tpl, "omit")) {
+		return {};
+	}
+
+	li := valueof(li_tpl);
+	for (i := 0; i < lengthof(li); i := i + 1) {
+		if (not ispresent(li[i]) or
+		    not match(li[i].id, id)) {
+			new_li := new_li & {li[i]};
+		}
+	}
+	return new_li;
+}
+
+function f_sip_param_set(template (omit) GenericParam_List li_tpl, charstring id, charstring val)
+return GenericParam_List {
+	var integer i;
+	var GenericParam_List li;
+	var GenericParam_List new_li := {};
+	var boolean found := false;
+
+	if (istemplatekind(li_tpl, "omit")) {
+		return { valueof(ts_Param(id, val)) };
+	}
+
+	li := valueof(li_tpl);
+	for (i := 0; i < lengthof(li); i := i + 1) {
+		if (not ispresent(li[i]) or
+		    not match(li[i].id, id)) {
+			new_li := new_li & {li[i]};
+			continue;
+		}
+		new_li := new_li & { valueof(ts_Param(li[i].id, val)) };
+		found := true;
+	}
+
+	if (not found) {
+		new_li := new_li & { valueof(ts_Param(id, val)) };
+	}
+	return new_li;
+}
+
+/* Make sure string is quoted. */
+function f_sip_str_quote(template (value) charstring val) return charstring {
+	var charstring str := valueof(val);
+	if (lengthof(str) == 0) {
+		return "";
+	}
+
+	if (str[0] != "\"") {
+		return "\"" & str & "\"";
+	}
+	return str;
+}
+
+/* Make sure string is unquoted.
+ * Similar to unq() in RFC 2617 */
+function f_sip_str_unquote(template (value) charstring val) return charstring {
+	var charstring str := valueof(val);
+	var integer len := lengthof(str);
+
+	if (len <= 1) {
+		return str;
+	}
+
+	if (str[0] == "\"" and str[len - 1] == "\"") {
+		return substr(str, 1, len - 2);
+	}
+	return str;
+}
+
+/* RFC 2617 3.2.2.2 A1 */
+function f_sip_digest_A1(charstring user, charstring realm, charstring password) return charstring {
+
+	/* RFC 2617 3.2.2.2 A1 */
+	var charstring A1 := f_sip_str_unquote(user) & ":" &
+			     f_sip_str_unquote(realm) & ":" &
+			     password;
+	var charstring digestA1 := f_str_tolower(f_calculateMD5(A1));
+	log("A1: md5('", A1, "') = ", digestA1);
+	return digestA1;
+}
+
+/* RFC 2617 3.2.2.2 A2 */
+function f_sip_digest_A2(charstring method, charstring uri) return charstring {
+
+	var charstring A2 := method & ":" & uri
+	var charstring digestA2 := f_str_tolower(f_calculateMD5(A2));
+	log("A2: md5('", A2, "') = ", digestA2);
+	return digestA2;
+}
+
+/* RFC 2617 3.2.2.1 Request-Digest */
+function f_sip_digest_RequestDigest(charstring digestA1, charstring nonce,
+				    charstring nc, charstring cnonce,
+				    charstring qop, charstring digestA2) return charstring {
+	var charstring digest_data := f_sip_str_unquote(nonce) & ":" &
+				      nc & ":" &
+				      cnonce & ":" &
+				      f_sip_str_unquote(qop) & ":" &
+				      digestA2;
+	var charstring req_digest := f_sip_digest_KD(digestA1, digest_data);
+	log("Request-Digest: md5('", digestA1, ":", digest_data ,"') = ", req_digest);
+	return req_digest;
+}
+
+/* RFC 2617 3.2.1 The WWW-Authenticate Response Header
+ * KD(secret, data) = H(concat(secret, ":", data))
+ */
+function f_sip_digest_KD(charstring secret, charstring data) return charstring {
+	return f_str_tolower(f_calculateMD5(secret & ":" & data));
+}
+
+/* Digest Auth: RFC 2617 */
+function f_sip_digest_gen_Authorization(WwwAuthenticate www_authenticate,
+				 charstring user, charstring password,
+				 charstring method, charstring uri,
+				 charstring cnonce := "0a4f113b", integer nc_int := 1) return Authorization {
+	var CommaParam_List digestCln;
+	var template (value) Authorization authorization;
+	var template (value) Credentials cred;
+	var template (omit) GenericParam rx_param;
+
+	digestCln := www_authenticate.challenge[0].digestCln;
+
+	var charstring algorithm;
+	rx_param := f_sip_param_find(digestCln, "algorithm");
+	if (istemplatekind(rx_param, "omit")) {
+		/* Assume MD5 if not set */
+		algorithm := "MD5"
+	} else {
+		algorithm := valueof(rx_param.paramValue);
+		if (f_strstr(algorithm, "MD5") == -1) {
+			Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail,
+						log2str("Unexpected algorithm: ", algorithm));
+		}
+	}
+
+	var charstring realm := f_sip_param_get_value_present_or_fail(digestCln, "realm");
+	var charstring nonce := f_sip_param_get_value_present_or_fail(digestCln, "nonce");
+	var charstring opaque := f_sip_param_get_value_present_or_fail(digestCln, "opaque");
+	var charstring qop := f_sip_param_get_value_present_or_fail(digestCln, "qop");
+
+	if (f_strstr(qop, "auth") == -1) {
+		Misc_Helpers.f_shutdown(__BFILE__, __LINE__, fail, log2str("Unexpected qop: ", qop));
+	}
+	var charstring selected_qop := "auth";
+
+	/* RFC 2617 3.2.2.2 A1 */
+	var charstring digestA1 := f_sip_digest_A1(user, realm, password);
+	/* RFC 2617 3.2.2.3 A2 */
+	var charstring digestA2 := f_sip_digest_A2(method, uri);
+
+	/* RFC 2617 3.2.2.1 Request-Digest */
+	var charstring nc := f_str_tolower(hex2str(int2hex(nc_int, 8)));
+	var charstring req_digest := f_sip_digest_RequestDigest(digestA1, nonce,
+								nc, cnonce,
+								selected_qop, digestA2);
+
+	cred := ts_Credentials_DigestResponseMD5(user, realm, nonce,
+						 uri, req_digest,
+						 opaque, algorithm, selected_qop, cnonce, nc);
+
+	authorization := ts_Authorization(cred);
+	return valueof(authorization);
+}
+
+/* RFC 2617 3.5 Example */
+function f_sip_digest_selftest() {
+/*
+The following example assumes that an access-protected document is
+being requested from the server via a GET request. The URI of the
+document is "http://www.nowhere.org/dir/index.html". Both client and
+server know that the username for this document is "Mufasa", and the
+password is "Circle Of Life" (with one space between each of the
+three words).
+
+HTTP/1.1 401 Unauthorized
+WWW-Authenticate: Digest
+	realm="testrealm@host.com",
+	qop="auth,auth-int",
+	nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
+	opaque="5ccc069c403ebaf9f0171e9517f40e41"
+
+Authorization: Digest username="Mufasa",
+	realm="testrealm@host.com",
+	nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
+	uri="/dir/index.html",
+	qop=auth,
+	nc=00000001,
+	cnonce="0a4f113b",
+	response="6629fae49393a05397450978507c4ef1",
+	opaque="5ccc069c403ebaf9f0171e9517f40e41"
+*/
+	var template (value) CommaParam_List digestCln := {
+		ts_Param("realm", f_sip_str_quote("testrealm@host.com")),
+		ts_Param("qop", f_sip_str_quote("auth,auth-int")),
+		ts_Param("nonce", f_sip_str_quote("dcd98b7102dd2f0e8b11d0f600bfb0c093")),
+		ts_Param("opaque", f_sip_str_quote("5ccc069c403ebaf9f0171e9517f40e41"))
+	};
+	var template (value) WwwAuthenticate www_authenticate :=
+		ts_WwwAuthenticate( { ts_Challenge_digestCln(digestCln) } )
+
+	var Authorization authorization :=
+		f_sip_digest_gen_Authorization(valueof(www_authenticate),
+					       "Mufasa",
+					       "Circle Of Life",
+					       "GET",
+					       "/dir/index.html",
+					       cnonce := "0a4f113b",
+					       nc_int := 1);
+
+	var CommaParam_List digestResp := authorization.body.digestResponse;
+	f_sip_param_match_value_or_fail(digestResp, "realm",	f_sip_str_quote("testrealm@host.com"));
+	f_sip_param_match_value_or_fail(digestResp, "nonce",	f_sip_str_quote("dcd98b7102dd2f0e8b11d0f600bfb0c093"));
+	f_sip_param_match_value_or_fail(digestResp, "uri",	f_sip_str_quote("/dir/index.html"));
+	f_sip_param_match_value_or_fail(digestResp, "qop",	"auth");
+	f_sip_param_match_value_or_fail(digestResp, "nc",	"00000001");
+	f_sip_param_match_value_or_fail(digestResp, "cnonce",	f_sip_str_quote("0a4f113b"));
+	f_sip_param_match_value_or_fail(digestResp, "response",	f_sip_str_quote("6629fae49393a05397450978507c4ef1"));
+	f_sip_param_match_value_or_fail(digestResp, "opaque",	f_sip_str_quote("5ccc069c403ebaf9f0171e9517f40e41"));
+}
+
 /* RFC 3261 8.1.1.5:
  * "The sequence number value MUST be expressible as a 32-bit unsigned integer
  *  and MUST be less than 2**31."
@@ -555,4 +998,68 @@
 	return f_rnd_int(2147483648)
 }
 
+/* Tags shall have at least 32 bit of randomness */
+function f_sip_rand_tag() return charstring {
+	var integer rnd_int := f_rnd_int(4294967296);
+	return hex2str(int2hex(rnd_int, 8));
+}
+
+/* Generate a "branch" tag value.
+ * RFC 3261 p.105 section 8:
+ * "A common way to create this value is to compute a
+ * cryptographic hash of the To tag, From tag, Call-ID header
+ * field, the Request-URI of the request received (before
+ * translation), the topmost Via header, and the sequence number
+ * from the CSeq header field, in addition to any Proxy-Require
+ * and Proxy-Authorization header fields that may be present.  The
+ * algorithm used to compute the hash is implementation-dependent,
+ * but MD5 (RFC 1321 [35]),expressed in hexadecimal, is a reasonable
+ * choice."
+ * See also Section 8.1.1.7:
+ * "The branch ID inserted by an element compliant with this
+ * specification MUST always begin with the characters "z9hG4bK"."
+ */
+const charstring sip_magic_cookie := "z9hG4bK";
+function f_sip_gen_branch(charstring tag_to,
+			  charstring tag_from,
+			  charstring tag_call_id,
+			  integer cseq) return charstring {
+	var charstring str := tag_to & tag_from & tag_call_id & int2str(cseq);
+	var charstring hash := f_calculateMD5(str);
+	var charstring branch := sip_magic_cookie & hash;
+	return branch;
+}
+
+function f_sip_HostPort_to_str(HostPort host_port) return charstring {
+	var charstring str := "";
+	if (ispresent(host_port.host)) {
+		str := host_port.host;
+	}
+	if (ispresent(host_port.portField)) {
+		str := str & ":" & int2str(host_port.portField);
+	}
+	return str;
+}
+
+function f_sip_SipUrl_to_str(SipUrl uri) return charstring {
+	var charstring str := uri.scheme & f_sip_HostPort_to_str(uri.hostPort);
+	return str;
+}
+
+function f_sip_NameAddr_to_str(NameAddr naddr) return charstring {
+	if (ispresent(naddr.displayName)) {
+		return naddr.displayName & " <" & f_sip_SipUrl_to_str(naddr.addrSpec) & ">";
+	} else {
+		return f_sip_SipUrl_to_str(naddr.addrSpec);
+	}
+}
+
+function f_sip_SipAddr_to_str(SipAddr sip_addr) return charstring {
+	if (ischosen(sip_addr.addr.nameAddr)) {
+		return f_sip_NameAddr_to_str(sip_addr.addr.nameAddr);
+	} else {
+		return f_sip_SipUrl_to_str(sip_addr.addr.addrSpecUnion);
+	}
+}
+
 }