<commit_before>"use strict";

var debug         = require('debug')('auth:signaturevalidator');
var Promise       = require('promise');
var hawk          = require('hawk');
var assert        = require('assert');
var _             = require('lodash');
require('superagent-hawk')(require('superagent'));
// Someone should rename utils to scopes... 
var utils         = require('taskcluster-lib-scopes');
var hoek          = require('hoek');
var https         = require('https');
var cryptiles     = require('cryptiles');
var crypto        = require('crypto');

/**
 * Limit the client scopes and possibly use temporary keys.
 *
 * Takes a client object on the form: `{clientId, accessToken, scopes}`,
 * applies scope restrictions, certificate validation and returns a clone if
 * modified (otherwise it returns the original).
 */
var parseExt = function(ext) {
  // Attempt to parse ext
  try {
    ext = JSON.parse(new Buffer(ext, 'base64').toString('utf-8'));
  }
  catch(err) {
    throw new Error("Failed to parse ext");
  }

  return ext;
}

/**
 * Limit the client scopes and possibly use temporary keys.
 *
 * Takes a client object on the form: `{clientId, accessToken, scopes}`,
 * applies scope restrictions, certificate validation and returns a clone if
 * modified (otherwise it returns the original).
 */
var limitClientWithExt = function(credentialName, issuingClientId, accessToken, scopes, ext, expandScopes) {
  let issuingScopes = scopes;
  let res = {scopes, accessToken};

  // Handle certificates
  if (ext.certificate) {
    var cert = ext.certificate;
    // Validate the certificate
    if (!(cert instanceof Object)) {
      throw new Error("ext.certificate must be a JSON object");
    }
    if (cert.version !== 1) {
      throw new Error("ext.certificate.version must be 1");
    }
    if (typeof(cert.seed) !== 'string') {
      throw new Error('ext.certificate.seed must be a string');
    }
    if (cert.seed.length !== 44) {
      throw new Error('ext.certificate.seed must be 44 characters');
    }
    if (typeof(cert.start) !== 'number') {
      throw new Error('ext.certificate.start must be a number');
    }
    if (typeof(cert.expiry) !== 'number') {
      throw new Error('ext.certificate.expiry must be a number');
    }
    if (!(cert.scopes instanceof Array)) {
      throw new Error("ext.certificate.scopes must be an array");
    }
    if (!cert.scopes.every(utils.validScope)) {
      throw new Error("ext.certificate.scopes must be an array of valid scopes");
    }

    // Check start and expiry
    var now = new Date().getTime();
    if (cert.start > now) {
      throw new Error("ext.certificate.start > now");
    }
    if (cert.expiry < now) {
      throw new Error("ext.certificate.expiry < now");
    }
    // Check max time between start and expiry
    if (cert.expiry - cert.start > 31 * 24 * 60 * 60 * 1000) {
      throw new Error("ext.certificate cannot last longer than 31 days!");
    }

    // Check clientId validity
    if (issuingClientId !== credentialName) {
      if (!cert.clientId) {
        throw new Error('ext.certificate.clientId must be set when ext.certificate.issuer is given');
      }
      if (typeof(cert.clientId) !== 'string') {
        throw new Error('ext.certificate.clientId must be a string');
      }
      if (cert.clientId !== credentialName) {
        throw new Error('ext.certificate.clientId must match the supplied clientId');
      }
      let createScope = 'auth:create-client:' + credentialName;
      if (!utils.scopeMatch(issuingScopes, [[createScope]])) {
        throw new Error("ext.certificate issuer `" + issuingClientId +
                        "` doesn't have `" + createScope + "` for supplied clientId.");
      }
    } else {
      if (cert.hasOwnProperty('clientId')) {
        throw new Error('ext.certificate.clientId must only be used with ext.certificate.issuer');
      }
    }

    // Validate certificate scopes are subset of client
    if (!utils.scopeMatch(scopes, [cert.scopes])) {
      throw new Error("ext.certificate issuer `" + issuingClientId +
                      "` doesn't have sufficient scopes");
    }

    // Generate certificate signature
    var sigContent = []
    sigContent.push('version:'    + '1');
    if (cert.clientId && cert.issuer) {
      sigContent.push('clientId:' + cert.clientId);
      sigContent.push('issuer:'   + cert.issuer);
    }
    sigContent.push('seed:'       + cert.seed);
    sigContent.push('start:'      + cert.start);
    sigContent.push('expiry:'     + cert.expiry);
    sigContent.push('scopes:');
    sigContent = sigContent.concat(cert.scopes);
    var signature = crypto.createHmac('sha256', accessToken)
      .update(sigContent.join('\n'))
      .digest('base64');

    // Validate signature
    if (typeof(cert.signature) !== 'string' ||
        !cryptiles.fixedTimeComparison(cert.signature, signature)) {
      throw new Error("ext.certificate.signature is not valid");
    }

    // Regenerate temporary key
    var temporaryKey = crypto.createHmac('sha256', accessToken)
      .update(cert.seed)
      .digest('base64')
      .replace(/\+/g, '-')  // Replace + with - (see RFC 4648, sec. 5)
      .replace(/\//g, '_')  // Replace / with _ (see RFC 4648, sec. 5)
      .replace(/=/g,  '');  // Drop '==' padding

    // Update scopes and accessToken
    res.accessToken = temporaryKey;
    res.scopes = scopes = expandScopes(cert.scopes);
  }

  // Handle scope restriction with authorizedScopes
  if (ext.authorizedScopes) {
    // Validate input format
    if (!(ext.authorizedScopes instanceof Array)) {
      throw new Error("ext.authorizedScopes must be an array");
    }
    if (!ext.authorizedScopes.every(utils.validScope)) {
      throw new Error("ext.authorizedScopes must be an array of valid scopes");
    }

    // Validate authorizedScopes scopes are satisfied by client (or temp) scopes
    if (!utils.scopeMatch(res.scopes, [ext.authorizedScopes])) {
      throw new Error("ext.authorizedScopes oversteps your scopes");
    }

    // Further limit scopes
    res.scopes = scopes = expandScopes(ext.authorizedScopes);
  }

  return res;
};


/**
 * Make a function for the signature validation.
 *
 * options:
 * {
 *    clientLoader:   async (clientId) => {clientId, accessToken, scopes},
 *    nonceManager:   nonceManager({size: ...}),
 *    expandScopes:   (scopes) => scopes,
 * }
 *
 * The function returned takes an object:
 *     {method, resource, host, port, authorization}
 * And returns promise for an object on one of the forms:
 *     {status: 'auth-failed', message}, or
 *     {status: 'auth-success', clientId, scheme, scopes}
 *
 * The `expandScopes` applies any rules that expands scopes, such as roles.
 * It is assumed that clients from `clientLoader` are returned with scopes
 * fully expanded.
 *
 * The method returned by this function works as `signatureValidator` for
 * `remoteAuthentication`.
 */
var createSignatureValidator = function(options) {
  assert(typeof(options) === 'object', "options must be an object");
  assert(options.clientLoader instanceof Function,
         "options.clientLoader must be a function");<commit_msg>restore info about hash<commit_after>"use strict";

var debug         = require('debug')('auth:signaturevalidator');
var Promise       = require('promise');
var hawk          = require('hawk');
var assert        = require('assert');
var _             = require('lodash');
require('superagent-hawk')(require('superagent'));
// Someone should rename utils to scopes... 
var utils         = require('taskcluster-lib-scopes');
var hoek          = require('hoek');
var https         = require('https');
var cryptiles     = require('cryptiles');
var crypto        = require('crypto');

/**
 * Limit the client scopes and possibly use temporary keys.
 *
 * Takes a client object on the form: `{clientId, accessToken, scopes}`,
 * applies scope restrictions, certificate validation and returns a clone if
 * modified (otherwise it returns the original).
 */
var parseExt = function(ext) {
  // Attempt to parse ext
  try {
    ext = JSON.parse(new Buffer(ext, 'base64').toString('utf-8'));
  }
  catch(err) {
    throw new Error("Failed to parse ext");
  }

  return ext;
}

/**
 * Limit the client scopes and possibly use temporary keys.
 *
 * Takes a client object on the form: `{clientId, accessToken, scopes}`,
 * applies scope restrictions, certificate validation and returns a clone if
 * modified (otherwise it returns the original).
 */
var limitClientWithExt = function(credentialName, issuingClientId, accessToken, scopes, ext, expandScopes) {
  let issuingScopes = scopes;
  let res = {scopes, accessToken};

  // Handle certificates
  if (ext.certificate) {
    var cert = ext.certificate;
    // Validate the certificate
    if (!(cert instanceof Object)) {
      throw new Error("ext.certificate must be a JSON object");
    }
    if (cert.version !== 1) {
      throw new Error("ext.certificate.version must be 1");
    }
    if (typeof(cert.seed) !== 'string') {
      throw new Error('ext.certificate.seed must be a string');
    }
    if (cert.seed.length !== 44) {
      throw new Error('ext.certificate.seed must be 44 characters');
    }
    if (typeof(cert.start) !== 'number') {
      throw new Error('ext.certificate.start must be a number');
    }
    if (typeof(cert.expiry) !== 'number') {
      throw new Error('ext.certificate.expiry must be a number');
    }
    if (!(cert.scopes instanceof Array)) {
      throw new Error("ext.certificate.scopes must be an array");
    }
    if (!cert.scopes.every(utils.validScope)) {
      throw new Error("ext.certificate.scopes must be an array of valid scopes");
    }

    // Check start and expiry
    var now = new Date().getTime();
    if (cert.start > now) {
      throw new Error("ext.certificate.start > now");
    }
    if (cert.expiry < now) {
      throw new Error("ext.certificate.expiry < now");
    }
    // Check max time between start and expiry
    if (cert.expiry - cert.start > 31 * 24 * 60 * 60 * 1000) {
      throw new Error("ext.certificate cannot last longer than 31 days!");
    }

    // Check clientId validity
    if (issuingClientId !== credentialName) {
      if (!cert.clientId) {
        throw new Error('ext.certificate.clientId must be set when ext.certificate.issuer is given');
      }
      if (typeof(cert.clientId) !== 'string') {
        throw new Error('ext.certificate.clientId must be a string');
      }
      if (cert.clientId !== credentialName) {
        throw new Error('ext.certificate.clientId must match the supplied clientId');
      }
      let createScope = 'auth:create-client:' + credentialName;
      if (!utils.scopeMatch(issuingScopes, [[createScope]])) {
        throw new Error("ext.certificate issuer `" + issuingClientId +
                        "` doesn't have `" + createScope + "` for supplied clientId.");
      }
    } else {
      if (cert.hasOwnProperty('clientId')) {
        throw new Error('ext.certificate.clientId must only be used with ext.certificate.issuer');
      }
    }

    // Validate certificate scopes are subset of client
    if (!utils.scopeMatch(scopes, [cert.scopes])) {
      throw new Error("ext.certificate issuer `" + issuingClientId +
                      "` doesn't have sufficient scopes");
    }

    // Generate certificate signature
    var sigContent = []
    sigContent.push('version:'    + '1');
    if (cert.clientId && cert.issuer) {
      sigContent.push('clientId:' + cert.clientId);
      sigContent.push('issuer:'   + cert.issuer);
    }
    sigContent.push('seed:'       + cert.seed);
    sigContent.push('start:'      + cert.start);
    sigContent.push('expiry:'     + cert.expiry);
    sigContent.push('scopes:');
    sigContent = sigContent.concat(cert.scopes);
    var signature = crypto.createHmac('sha256', accessToken)
      .update(sigContent.join('\n'))
      .digest('base64');

    // Validate signature
    if (typeof(cert.signature) !== 'string' ||
        !cryptiles.fixedTimeComparison(cert.signature, signature)) {
      throw new Error("ext.certificate.signature is not valid");
    }

    // Regenerate temporary key
    var temporaryKey = crypto.createHmac('sha256', accessToken)
      .update(cert.seed)
      .digest('base64')
      .replace(/\+/g, '-')  // Replace + with - (see RFC 4648, sec. 5)
      .replace(/\//g, '_')  // Replace / with _ (see RFC 4648, sec. 5)
      .replace(/=/g,  '');  // Drop '==' padding

    // Update scopes and accessToken
    res.accessToken = temporaryKey;
    res.scopes = scopes = expandScopes(cert.scopes);
  }

  // Handle scope restriction with authorizedScopes
  if (ext.authorizedScopes) {
    // Validate input format
    if (!(ext.authorizedScopes instanceof Array)) {
      throw new Error("ext.authorizedScopes must be an array");
    }
    if (!ext.authorizedScopes.every(utils.validScope)) {
      throw new Error("ext.authorizedScopes must be an array of valid scopes");
    }

    // Validate authorizedScopes scopes are satisfied by client (or temp) scopes
    if (!utils.scopeMatch(res.scopes, [ext.authorizedScopes])) {
      throw new Error("ext.authorizedScopes oversteps your scopes");
    }

    // Further limit scopes
    res.scopes = scopes = expandScopes(ext.authorizedScopes);
  }

  return res;
};


/**
 * Make a function for the signature validation.
 *
 * options:
 * {
 *    clientLoader:   async (clientId) => {clientId, accessToken, scopes},
 *    nonceManager:   nonceManager({size: ...}),
 *    expandScopes:   (scopes) => scopes,
 * }
 *
 * The function returned takes an object:
 *     {method, resource, host, port, authorization}
 * And returns promise for an object on one of the forms:
 *     {status: 'auth-failed', message},
 *     {status: 'auth-success', clientId, scheme, scopes}, or
 *     {status: 'auth-success', clientId, scheme, scopes, hash}
 * where `hash` is the payload hash.
 *
 * The `expandScopes` applies any rules that expands scopes, such as roles.
 * It is assumed that clients from `clientLoader` are returned with scopes
 * fully expanded.
 *
 * The method returned by this function works as `signatureValidator` for
 * `remoteAuthentication`.
 */
var createSignatureValidator = function(options) {
  assert(typeof(options) === 'object', "options must be an object");
  assert(options.clientLoader instanceof Function,
         "options.clientLoader must be a function");
{'fraction_non_alphanumeric': 0.1317614424410541, 'fraction_numerical': 0.006102635228848821, 'mean_word_length': 2.974917309812569, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2729409', 'n_tokens_mistral': 4300, 'n_tokens_neox': 4137, 'n_words': 1244}