tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_auth_multiple.js (11811B)


      1 // This file tests authentication prompt callbacks
      2 // TODO NIT use do_check_eq(expected, actual) consistently, not sometimes eq(actual, expected)
      3 
      4 "use strict";
      5 
      6 const { HttpServer } = ChromeUtils.importESModule(
      7  "resource://testing-common/httpd.sys.mjs"
      8 );
      9 
     10 // Turn off the authentication dialog blocking for this test.
     11 var prefs = Services.prefs;
     12 prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
     13 
     14 function URL(domain, path = "") {
     15  if (path.startsWith("/")) {
     16    path = path.substring(1);
     17  }
     18  return `http://${domain}:${httpserv.identity.primaryPort}/${path}`;
     19 }
     20 
     21 ChromeUtils.defineLazyGetter(this, "PORT", function () {
     22  return httpserv.identity.primaryPort;
     23 });
     24 
     25 const FLAG_RETURN_FALSE = 1 << 0;
     26 const FLAG_WRONG_PASSWORD = 1 << 1;
     27 const FLAG_BOGUS_USER = 1 << 2;
     28 // const FLAG_PREVIOUS_FAILED = 1 << 3;
     29 const CROSS_ORIGIN = 1 << 4;
     30 // const FLAG_NO_REALM = 1 << 5;
     31 const FLAG_NON_ASCII_USER_PASSWORD = 1 << 6;
     32 
     33 function AuthPrompt1(flags) {
     34  this.flags = flags;
     35 }
     36 
     37 AuthPrompt1.prototype = {
     38  user: "guest",
     39  pass: "guest",
     40 
     41  expectedRealm: "secret",
     42 
     43  QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
     44 
     45  prompt: function ap1_prompt() {
     46    do_throw("unexpected prompt call");
     47  },
     48 
     49  promptUsernameAndPassword: function ap1_promptUP(
     50    title,
     51    text,
     52    realm,
     53    savePW,
     54    user,
     55    pw
     56  ) {
     57    if (!(this.flags & CROSS_ORIGIN)) {
     58      if (!text.includes(this.expectedRealm)) {
     59        do_throw("Text must indicate the realm");
     60      }
     61    } else if (text.includes(this.expectedRealm)) {
     62      do_throw("There should not be realm for cross origin");
     63    }
     64    if (!text.includes("localhost")) {
     65      do_throw("Text must indicate the hostname");
     66    }
     67    if (!text.includes(String(PORT))) {
     68      do_throw("Text must indicate the port");
     69    }
     70    if (text.includes("-1")) {
     71      do_throw("Text must contain negative numbers");
     72    }
     73 
     74    if (this.flags & FLAG_RETURN_FALSE) {
     75      return false;
     76    }
     77 
     78    if (this.flags & FLAG_BOGUS_USER) {
     79      this.user = "foo\nbar";
     80    } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
     81      this.user = "é";
     82    }
     83 
     84    user.value = this.user;
     85    if (this.flags & FLAG_WRONG_PASSWORD) {
     86      pw.value = this.pass + ".wrong";
     87      // Now clear the flag to avoid an infinite loop
     88      this.flags &= ~FLAG_WRONG_PASSWORD;
     89    } else if (this.flags & FLAG_NON_ASCII_USER_PASSWORD) {
     90      pw.value = "é";
     91    } else {
     92      pw.value = this.pass;
     93    }
     94    return true;
     95  },
     96 
     97  promptPassword: function ap1_promptPW() {
     98    do_throw("unexpected promptPassword call");
     99  },
    100 };
    101 
    102 function AuthPrompt2(flags) {
    103  this.flags = flags;
    104 }
    105 
    106 AuthPrompt2.prototype = {
    107  user: "guest",
    108  pass: "guest",
    109 
    110  expectedRealm: "secret",
    111 
    112  QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]),
    113 
    114  promptAuth: function ap2_promptAuth(channel, level, authInfo) {
    115    authInfo.username = this.user;
    116    authInfo.password = this.pass;
    117    return true;
    118  },
    119 
    120  asyncPromptAuth: function ap2_async() {
    121    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
    122  },
    123 };
    124 
    125 function Requestor(flags, versions) {
    126  this.flags = flags;
    127  this.versions = versions;
    128 }
    129 
    130 Requestor.prototype = {
    131  QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]),
    132 
    133  getInterface: function requestor_gi(iid) {
    134    if (this.versions & 1 && iid.equals(Ci.nsIAuthPrompt)) {
    135      // Allow the prompt to store state by caching it here
    136      if (!this.prompt1) {
    137        this.prompt1 = new AuthPrompt1(this.flags);
    138      }
    139      return this.prompt1;
    140    }
    141    if (this.versions & 2 && iid.equals(Ci.nsIAuthPrompt2)) {
    142      // Allow the prompt to store state by caching it here
    143      if (!this.prompt2) {
    144        this.prompt2 = new AuthPrompt2(this.flags);
    145      }
    146      return this.prompt2;
    147    }
    148 
    149    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
    150  },
    151 
    152  prompt1: null,
    153  prompt2: null,
    154 };
    155 
    156 function RealmTestRequestor() {}
    157 
    158 RealmTestRequestor.prototype = {
    159  QueryInterface: ChromeUtils.generateQI([
    160    "nsIInterfaceRequestor",
    161    "nsIAuthPrompt2",
    162  ]),
    163 
    164  getInterface: function realmtest_interface(iid) {
    165    if (iid.equals(Ci.nsIAuthPrompt2)) {
    166      return this;
    167    }
    168 
    169    throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE);
    170  },
    171 
    172  promptAuth: function realmtest_checkAuth(channel, level, authInfo) {
    173    Assert.equal(authInfo.realm, '"foo_bar');
    174 
    175    return false;
    176  },
    177 
    178  asyncPromptAuth: function realmtest_async() {
    179    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
    180  },
    181 };
    182 
    183 function makeChan(url) {
    184  let loadingUrl = Services.io
    185    .newURI(url)
    186    .mutate()
    187    .setPathQueryRef("")
    188    .finalize();
    189  var principal = Services.scriptSecurityManager.createContentPrincipal(
    190    loadingUrl,
    191    {}
    192  );
    193  return NetUtil.newChannel({
    194    uri: url,
    195    loadingPrincipal: principal,
    196    securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
    197    contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
    198  });
    199 }
    200 
    201 function ntlm_auth(metadata, response) {
    202  let challenge = metadata.getHeader("Authorization");
    203  if (!challenge.startsWith("NTLM ")) {
    204    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    205    return;
    206  }
    207 
    208  let decoded = atob(challenge.substring(5));
    209  info(decoded);
    210 
    211  if (!decoded.startsWith("NTLMSSP\0")) {
    212    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    213    return;
    214  }
    215 
    216  let isNegotiate = decoded.substring(8).startsWith("\x01\x00\x00\x00");
    217  let isAuthenticate = decoded.substring(8).startsWith("\x03\x00\x00\x00");
    218 
    219  if (isNegotiate) {
    220    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    221    response.setHeader(
    222      "WWW-Authenticate",
    223      "NTLM TlRMTVNTUAACAAAAAAAAAAAoAAABggAAASNFZ4mrze8AAAAAAAAAAAAAAAAAAAAA",
    224      false
    225    );
    226    return;
    227  }
    228 
    229  if (isAuthenticate) {
    230    let body = "OK";
    231    response.bodyOutputStream.write(body, body.length);
    232    return;
    233  }
    234 
    235  // Something else went wrong.
    236  response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    237 }
    238 
    239 function basic_auth(metadata, response) {
    240  let challenge = metadata.getHeader("Authorization");
    241  if (!challenge.startsWith("Basic ")) {
    242    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    243    return;
    244  }
    245 
    246  if (challenge == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
    247    response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
    248    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
    249 
    250    let body = "success";
    251    response.bodyOutputStream.write(body, body.length);
    252    return;
    253  }
    254 
    255  response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    256  response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
    257 }
    258 
    259 //
    260 // Digest functions
    261 //
    262 function bytesFromString(str) {
    263  const encoder = new TextEncoder();
    264  return encoder.encode(str);
    265 }
    266 
    267 // return the two-digit hexadecimal code for a byte
    268 function toHexString(charCode) {
    269  return ("0" + charCode.toString(16)).slice(-2);
    270 }
    271 
    272 function H(str) {
    273  var data = bytesFromString(str);
    274  var ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
    275  ch.init(Ci.nsICryptoHash.MD5);
    276  ch.update(data, data.length);
    277  var hash = ch.finish(false);
    278  return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join("");
    279 }
    280 
    281 const nonce = "6f93719059cf8d568005727f3250e798";
    282 const opaque = "1234opaque1234";
    283 const digestChallenge = `Digest realm="secret", domain="/",  qop=auth,algorithm=MD5, nonce="${nonce}" opaque="${opaque}"`;
    284 //
    285 // Digest handler
    286 //
    287 // /auth/digest
    288 function authDigest(metadata, response) {
    289  var cnonceRE = /cnonce="(\w+)"/;
    290  var responseRE = /response="(\w+)"/;
    291  var usernameRE = /username="(\w+)"/;
    292  var body = "";
    293  // check creds if we have them
    294  if (metadata.hasHeader("Authorization")) {
    295    var auth = metadata.getHeader("Authorization");
    296    var cnonce = auth.match(cnonceRE)[1];
    297    var clientDigest = auth.match(responseRE)[1];
    298    var username = auth.match(usernameRE)[1];
    299    var nc = "00000001";
    300 
    301    if (username != "guest") {
    302      response.setStatusLine(metadata.httpVersion, 400, "bad request");
    303      body = "should never get here";
    304    } else {
    305      // see RFC2617 for the description of this calculation
    306      var A1 = "guest:secret:guest";
    307      var A2 = "GET:/path";
    308      var noncebits = [nonce, nc, cnonce, "auth", H(A2)].join(":");
    309      var digest = H([H(A1), noncebits].join(":"));
    310 
    311      if (clientDigest == digest) {
    312        response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
    313        body = "digest";
    314      } else {
    315        info(clientDigest);
    316        info(digest);
    317        handle_unauthorized(metadata, response);
    318        return;
    319      }
    320    }
    321  } else {
    322    // no header, send one
    323    handle_unauthorized(metadata, response);
    324    return;
    325  }
    326 
    327  response.bodyOutputStream.write(body, body.length);
    328 }
    329 
    330 let challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
    331 
    332 function handle_unauthorized(metadata, response) {
    333  response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    334 
    335  for (let ch of challenges) {
    336    response.setHeader("WWW-Authenticate", ch, true);
    337  }
    338 }
    339 
    340 // /path
    341 function auth_handler(metadata, response) {
    342  if (!metadata.hasHeader("Authorization")) {
    343    handle_unauthorized(metadata, response);
    344    return;
    345  }
    346 
    347  let challenge = metadata.getHeader("Authorization");
    348  if (challenge.startsWith("NTLM ")) {
    349    ntlm_auth(metadata, response);
    350    return;
    351  }
    352 
    353  if (challenge.startsWith("Basic ")) {
    354    basic_auth(metadata, response);
    355    return;
    356  }
    357 
    358  if (challenge.startsWith("Digest ")) {
    359    authDigest(metadata, response);
    360    return;
    361  }
    362 
    363  handle_unauthorized(metadata, response);
    364 }
    365 
    366 let httpserv;
    367 add_setup(() => {
    368  Services.prefs.setBoolPref("network.auth.force-generic-ntlm", true);
    369  Services.prefs.setBoolPref("network.auth.force-generic-ntlm-v1", true);
    370  Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
    371  Services.prefs.setBoolPref("network.http.sanitize-headers-in-logs", false);
    372 
    373  httpserv = new HttpServer();
    374  httpserv.registerPathHandler("/path", auth_handler);
    375  httpserv.start(-1);
    376 
    377  registerCleanupFunction(async () => {
    378    Services.prefs.clearUserPref("network.auth.force-generic-ntlm");
    379    Services.prefs.clearUserPref("network.auth.force-generic-ntlm-v1");
    380    Services.prefs.clearUserPref("network.dns.native-is-localhost");
    381    Services.prefs.clearUserPref("network.http.sanitize-headers-in-logs");
    382 
    383    await httpserv.stop();
    384  });
    385 });
    386 
    387 add_task(async function test_ntlm_first() {
    388  Services.prefs.setBoolPref(
    389    "network.auth.choose_most_secure_challenge",
    390    false
    391  );
    392  challenges = ["NTLM", `Basic realm="secret"`, digestChallenge];
    393  httpserv.identity.add("http", "ntlm.com", httpserv.identity.primaryPort);
    394  let chan = makeChan(URL("ntlm.com", "/path"));
    395 
    396  chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
    397  let [req, buf] = await new Promise(resolve => {
    398    chan.asyncOpen(
    399      new ChannelListener((request, buffer) => resolve([request, buffer]), null)
    400    );
    401  });
    402  Assert.equal(buf, "OK");
    403  Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
    404 });
    405 
    406 add_task(async function test_choose_most_secure() {
    407  // By default, we rank the challenges by how secure they are.
    408  // In this case, NTLM should be the most secure.
    409  challenges = [digestChallenge, `Basic realm="secret"`, "NTLM"];
    410  httpserv.identity.add(
    411    "http",
    412    "ntlmstrong.com",
    413    httpserv.identity.primaryPort
    414  );
    415  let chan = makeChan(URL("ntlmstrong.com", "/path"));
    416 
    417  chan.notificationCallbacks = new Requestor(FLAG_RETURN_FALSE, 2);
    418  let [req, buf] = await new Promise(resolve => {
    419    chan.asyncOpen(
    420      new ChannelListener((request, buffer) => resolve([request, buffer]), null)
    421    );
    422  });
    423  Assert.equal(req.QueryInterface(Ci.nsIHttpChannel).responseStatus, 200);
    424  Assert.equal(buf, "OK");
    425 });