const childProcess = require('child_process');
const spawn = require('child_process').spawn;
const S = require('string');
const os = require('os');
const path = require('path');
const pem = require('pem');
const crypto = require('crypto');
const async = require('async');
const _ = require('underscore');
const fs = require('fs');
const request = require('request');
const Utils = require('./utils');
// keep track of root certificates
if(!global.PASSMARKED_ROOT_CERTS) global.PASSMARKED_ROOT_CERTS = [];
module.exports = exports = function(payload) {
/**
* Get our payload data
**/
var data = payload.getData();
/**
* Object to return
**/
var SSL = {};
/**
* Merge the libraries
**/
SSL = _.extend(SSL, Utils(payload));
/**
* Cache of root certificates
**/
var roots = [];
/**
* Returns a presentable form of our internal certificate layout
**/
SSL.buildPresentableCertificate = function(cert) {
// build the output
var output = {
pem: cert.pem,
source: cert.collection,
type: cert.type,
commonName: cert.commonName,
alt: ((cert.san || {}).dns || []),
title: cert.commonName,
description: cert.commonName,
index: cert.index,
verified: cert.verified === true,
revoked: cert.revoked === true,
signature: cert.signature || null,
bits: cert.strength || null,
crl: cert.crl || null,
ocsp: cert.ocsp || null,
url: cert.url || null
};
// returns the output
return output;
};
/**
* Builds the presentable chain
**/
SSL.buildPresentableChain = function(expectedPath, suppliedPath) {
// the final chain to return
var chain = [];
// sort the chain
var sortedChain = _.sortBy(expectedPath, 'index');
// loop the expected certificate
for(var i = 0; i < sortedChain.length; i++) {
// use this as the certificate
var cert = _.find(suppliedPath || [], function(item) {
return item.commonName === sortedChain[i].commonName;
});
// did we find this cert ?
if(cert) {
// add our found certificate
chain.push(SSL.buildPresentableCertificate(cert));
} else {
// add the certificate from the chain as it was not present
chain.push(SSL.buildPresentableCertificate(sortedChain[i]));
}
}
// returns the chain we generated
return chain;
};
/**
* Returns true if the given certificate fingerprint is present
**/
SSL.findByFingerprint = function(certificates, fingerprints, fn) {
// create a slug we can use to check
var slug = _.pluck(fingerprints || {}, 'hash').join('-');
// loop the certificates
for(var i = 0; i < certificates.length; i++) {
// loop the fingerprints
for(var a = 0; a < (certificates[i].fingerprints || []).length; a++) {
// check if any of the fingerprints match
if(slug.indexOf(certificates[i].fingerprints[a].hash) !== -1) {
// return the certificate
return certificates[i];
}
}
}
// the default to return is null
return null;
};
/**
* Returns the possible SSL fingerprints
**/
SSL.getFingerPrintsFromPEM = function(cert, fn) {
// list of hashes to add
var fingerprints = [];
// general all the fingerprints
async.each([
'md5', 'sha1', 'sha256'
], function (algorithm, cb) {
// set the fingerprint
pem.getFingerprint(cert, algorithm, function(err, prints) {
// add to list
if(prints &&
S(prints.fingerprint || '').isEmpty() === false)
fingerprints.push({
algorithm: algorithm,
hash: prints.fingerprint
});
// done
cb(err);
});
}, function(err) {
// finish
fn(err, fingerprints);
});
};
/**
* Returns the timeout to use
**/
SSL.getTimeoutCommand = function() {
// the path for timeout
var platform = os.platform();
var timeoutCommand = 'timeout';
// fallback to gtimeout on osx
if(platform === 'linux') {
// set to gtimeout
timeoutCommand = 'timeout ';
} else if(platform === 'darwin') {
// set to gtimeout
timeoutCommand = '/usr/local/bin/gtimeout ';
}
// done
return timeoutCommand;
};
/**
* Returns the exec from bin
**/
SSL.getExecutable = function(version) {
// return the full command
return SSL.getTimeoutCommand() + ' 10 ' + SSL.getSSLExecutable(version);
};
/**
* Execs the OPENSSL and first check the first testing stdout
**/
SSL.exec = function(cmd, fn) {
// load out
payload.debug('Running command: ' + cmd);
// check if stdout and stderror was defined
if(data.testingStdout || data.testingStderr) {
// return the callback
return fn(
null,
(data.testingStdout || '').toString(),
(data.testingStderr || '').toString()
);
}
// execute the actual process
childProcess.exec(cmd, {}, function(err, stdout, stderr) {
// check the error
if(err) {
// done
return fn(err);
}
// to string just in case
stdout = (stdout || '').toString();
stderr = (stderr || '').toString();
// done
fn(err, stdout, stderr);
});
};
/**
* Returns all the certificates from the peer certificates
**/
SSL.getPeerCertificates = function(cert, fn) {
// get the certificates
var certs = [];
var cnames = [];
// the current item we are looking at
var currentCertificate = cert;
// keep count
var count = 0;
var run = 0;
// loop it
while(currentCertificate !== null) {
// check if not in list already
if(cnames.indexOf(currentCertificate.subject.CN) === -1) {
// add our mod
cnames.push(currentCertificate.subject.CN);
// add the certificate
certs.push(_.extend({}, currentCertificate, {
index: count
}));
// increment
count++;
}
// internal run count
run++;
// update the current certificate
currentCertificate = currentCertificate.issuerCertificate || null;
// stop it
if(currentCertificate &&
(currentCertificate.issuer || {}).CN === currentCertificate.subject.CN) {
// done
break;
}
// break if more than 30, just in case this is a "forever" loop :\
if(run > 30) break;
}
// done
fn(null, certs);
};
/**
* Parses the PEM and returns valid information
**/
SSL.readCertificateInfo = function(pemCertificate, fn) {
// get the details from pem
pem.readCertificateInfo(pemCertificate, function(err, info) {
// check for a error
if(err) {
// report
payload.error('Problem parsing details out of Pem Format', err);
// done
return fn(err);
}
// right so we got the info
info = (info || {});
// extract more variables
SSL.extractPEMVariables(pemCertificate, function(err, meta) {
// set our properties
info = _.extend(info, meta);
// done
fn(null, info);
});
});
};
/**
*
**/
SSL.verifyChain = function(certs, info, fn) {
// create the chain
var userCert = certs[0];
var middleCerts = certs.slice(1, certs.length).join('\n');
// get certificates by the same company
fn(null);
};
/**
* Downloads all the known certificate paths
**/
SSL.downloadPath = function(cert, index, fn) {
// the list for the path
var paths = [];
// sanity check
if(!cert) return fn(null, paths);
// limit the stack if more than 20 already
if(index > 20) return fn(null, paths);
// check if this is a peer certificate
if(index === 0) {
// convert the certificate
return SSL.convertDERtoPEM(cert.raw, function(err, pemCertificate) {
// check for a error
if(err) {
// debug
payload.error('Something went wrong while converting the DER to PEM', err);
// done
return fn(err, paths);
}
// get the finger prints of the passed certificates
SSL.getFingerPrintsFromPEM(pemCertificate, function(err, prints) {
// handle error
if(err) {
// output to stderr
payload.error('Problem generating fingerprints of PEM file', err);
// done
return fn(err, paths);
}
// get the details from pem
SSL.readCertificateInfo(pemCertificate, function(err, info) {
// handle error
if(err) {
// output to stderr
payload.error('Problem generating certificate info of PEM file', err);
// done
return fn(err, paths);
}
// add to list
paths.push(_.extend({}, info, {
pem: pemCertificate,
der: cert.raw.toString('base64'),
fingerprints: prints,
collection: 'supplied',
index: index,
type: 'user'
}));
// trigger the next certificate walk
SSL.downloadPath(info, index + 1, function(err, returnedPaths) {
// find any other legacy roots
SSL.verifyChain(_.pluck(returnedPaths, 'pem'), info, function() {
// done
fn(err, paths.concat(returnedPaths || []));
});
});
});
});
});
} else if(S(cert.parent || '').isEmpty() === false) {
// downloads the certificate
SSL.downloadCertificate(cert.parent, function(err, downloadedCert) {
// check for a error
if(err) {
// output
payload.error('Problem downloading the certificate from ' + path, err);
// done
return fn(err);
}
// convert the certificate
SSL.convertDERtoPEM(downloadedCert, function(err, pemCertificate) {
// check for a error
if(err) {
// debug
payload.error('Something went wrong while converting the DER to PEM', err);
// done
return fn(err);
}
// get the finger prints of the passed certificates
SSL.getFingerPrintsFromPEM(pemCertificate, function(err, prints) {
// get the details from pem
SSL.readCertificateInfo(pemCertificate, function(err, info) {
// check for a error
if(err) {
// debug
payload.error('Something went wrong checking the certificate info', err);
// done
return cb(err);
}
SSL.getRootCertificateByIssuer(info, function(err, rootCertificate) {
// get the type
var certType = 'user';
if(info.commonName === ((info || {}).issuer || {}).commonName)
infoType = 'root';
else
infoType = 'intermediate';
// add to list
paths.push(_.extend({}, info, {
url: cert.parent,
pem: pemCertificate.split('\n'),
der: downloadedCert.toString('base64'),
fingerprints: prints,
collection: 'expected',
index: index,
type: infoType
}));
// if not the root already ?
// if(info.commonName == (info.issuer || {}).commonName) return fn(null, paths);
// check the path
if(S(info.parent || '').isEmpty() === true) {
SSL.walkTrustedChain(info.issuer || {}, index + 1, function(err, returnedPaths) {
// done
fn(err, paths.concat(returnedPaths || []));
});
} else {
// trigger the next certificate walk
SSL.downloadPath(info, index + 1, function(err, returnedPaths) {
// done
fn(err, paths.concat(returnedPaths || []));
});
}
});
});
});
});
});
} else {
// done
fn(null, paths);
}
};
/**
* Checks down the chain
**/
SSL.walkTrustedChain = function(info, index, fn) {
// the list of paths to return
var paths = [];
// check if not over limit
if(index > 20) {
// stop at the limit, 20
return fn(null, paths);
}
// return the info if not defined
if(!info) {
// finish returning the path
return fn(null, paths);
}
// check if registed as a trusted certificate
SSL.getRootCertificateByIssuer(info, function(err, cert) {
// sanity check
if(!cert) return fn(null, paths);
// create fingerprints
SSL.getFingerPrintsFromPEM(cert.pem, function(err, prints) {
// get the type
var certType = 'user';
if(cert.index === 0)
certType = 'user';
else if(cert.commonName === ((cert || {}).issuer || {}).commonName)
certType = 'root';
else
certType = 'intermediate';
// add to list
paths.push(_.extend({}, info, {
pem: cert.pem.split('\n'),
der: cert.der,
fingerprints: prints,
collection: 'expected',
index: index,
type: certType
}));
// check if not the root
if(cert.issuer &&
cert.issuer.commonName !== cert.commonName) {
// start to walk the actual tree
return SSL.walkTrustedChain(cert.issuer, index + 1, function(err, returnedCert) {
// return the paths
fn(null, paths.concat(returnedCert || []));
});
}
// return the paths
fn(null, paths);
});
});
};
/**
* Returns all the certificates from the peer certificates
**/
SSL.parseCertificates = function(peers, fn) {
// get the certificates
var certs = [];
// loop the certificates
async.each(peers, function(cert, cb) {
// do te request and get all the certificates
SSL.convertDERtoPEM(cert.raw, function(err, pemCertificate) {
// check for a error
if(err) {
// debug
payload.error('Something went wrong while converting the DER to PEM', err);
// done
return cb(err);
}
// get the details from pem
SSL.readCertificateInfo(pemCertificate, function(err, info) {
// check for a error
if(err) {
// debug
payload.error('Something went wrong checking the certificate info', err);
// done
return cb(err);
}
// create fingerprints
SSL.getFingerPrintsFromPEM(pemCertificate, function(err, prints) {
SSL.getRootCertificateByIssuer(info, function(err, rootCertificate) {
// get the type
var certType = 'user';
if(cert.index === 0)
certType = 'user';
else if(rootCertificate &&
cert.commonName === ((cert || {}).issuer || {}).commonName)
certType = 'root';
else
certType = 'intermediate';
// add to list
certs.push(_.extend({}, info, {
pem: pemCertificate.split('\n'),
der: cert.raw.toString('base64'),
fingerprints: prints,
collection: 'supplied',
index: cert.index,
type: certType
}));
// done
cb(null);
});
});
});
});
}, function() {
// done
fn(null, certs);
});
};
/**
* Returns the CRT for the path given, but first check the cache
**/
SSL.downloadCertificate = function(path, fn) {
// create the sha1 hash
var shasum = crypto.createHash('sha1');
shasum.update(path);
var hash = shasum.digest('hex');
// build the cache key
var cacheKey = [
'passmarked',
'certificates',
'der',
hash
].join(':');
// first check the cache for the certificate
payload.get(cacheKey, function(err, cachedBody) {
// did we get a error ?
if(!err && cachedBody) {
// return the cached version
return fn(null, new Buffer(cachedBody, 'base64'));
}
// do the actual request and processing
request({
url: path,
timeout: 10 * 1000,
encoding: null
}, function(err, response, body) {
// check if we have a error
if(err) {
// output to stderr
payload.error('Problem downloading the given certificate from ' + path, err);
// done
return fn(err);
}
// check if we got the certificate
if((response || {}).statusCode === 200) {
// all good set in the cache and return
return payload.set(cacheKey, body.toString('base64'), function(err) {
// just output the error setting this in the cache
if(err) payload.error('Problem settings the certificate from ' + path + ' in the cache', err);
// just move on
fn(null, body);
});
}
// nope was not able to get the certificate
fn(null);
});
});
};
// return object instance
return SSL;
};
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import _ from 'lodash';
import B from 'bluebird';
import { MOCHA_TIMEOUT, initSession, deleteSession } from '../helpers/session';
import { doesIncludeCookie, doesNotIncludeCookie,
newCookie, oldCookie1 } from './safari-basic-e2e-specs';
import { SAFARI_CAPS } from '../desired';
import https from 'https';
const pem = B.promisifyAll(require('pem'));
chai.should();
chai.use(chaiAsPromised);
const HTTPS_PORT = 9762;
const LOCAL_HTTPS_URL = `https://localhost:${HTTPS_PORT}/`;
const caps = _.defaults({
safariInitialUrl: LOCAL_HTTPS_URL,
noReset: true,
}, SAFARI_CAPS);
let pemCertificate;
if (!process.env.REAL_DEVICE && !process.env.CLOUD) {
describe.skip('Safari SSL', function () {
this.timeout(MOCHA_TIMEOUT);
let sslServer, driver;
before(async function () {
// Create a random pem certificate
const privateKey = await pem.createPrivateKeyAsync();
const keys = await pem.createCertificateAsync({
days: 1,
selfSigned: true,
serviceKey: privateKey.key,
altNames: ['localhost'],
});
pemCertificate = keys.certificate;
// Host an SSL server that uses that certificate
const serverOpts = {key: keys.serviceKey, cert: pemCertificate};
sslServer = https.createServer(serverOpts, (req, res) => {
res.end('Arbitrary text');
}).listen(HTTPS_PORT);
caps.customSSLCert = pemCertificate;
});
after(async function () {
await deleteSession();
if (sslServer) {
await sslServer.close();
}
});
it('should open pages with untrusted certs if the cert was provided in desired capabilities', async function () {
try {
driver = await initSession(caps);
await driver.source().should.eventually.include('Arbitrary text');
await driver.quit();
await B.delay(1000);
// Now do another session using the same cert to verify that it still works
// (Don't do it on CLOUD. Restarting is too slow)
if (!process.env.CLOUD) {
await driver.init(caps);
await driver.get(LOCAL_HTTPS_URL);
await driver.source().should.eventually.include('Arbitrary text');
}
} finally {
await deleteSession();
}
});
describe('cookies', function () {
const secureCookie = Object.assign({}, newCookie, {
secure: true,
name: 'securecookie',
value: 'this is a secure cookie',
});
before(async function () {
driver = await initSession(caps);
});
beforeEach(async function () {
await driver.get(LOCAL_HTTPS_URL);
await driver.setCookie(oldCookie1);
await driver.deleteCookie(secureCookie.name);
});
it('should be able to set a secure cookie', async function () {
let cookies = await driver.allCookies();
doesNotIncludeCookie(cookies, secureCookie);
await driver.setCookie(secureCookie);
cookies = await driver.allCookies();
doesIncludeCookie(cookies, secureCookie);
// should not clobber old cookie
doesIncludeCookie(cookies, oldCookie1);
});
it('should be able to set a secure cookie', async function () {
await driver.setCookie(secureCookie);
let cookies = await driver.allCookies();
doesIncludeCookie(cookies, secureCookie);
// should not clobber old cookie
doesIncludeCookie(cookies, oldCookie1);
await driver.deleteCookie(secureCookie.name);
cookies = await driver.allCookies();
doesNotIncludeCookie(cookies, secureCookie);
});
});
});
}
const crypto = require('crypto');
const base64url = require('base64url');
const cbor = require('cbor');
const fs = require('fs');
const { Certificate } = require('@fidm/x509');
let userVerificationDefault = "preferred"; //userVerification - can be set to "required", "preferred", "discouraged". More in WebAuthn specification. Default set to "preferred"
/**
* U2F Presence constant
*/
let U2F_USER_PRESENTED = 0x01;
/**
* Takes signature, data and PEM public key and tries to verify signature
* @param {Buffer} signature
* @param {Buffer} data
* @param {String} publicKey - PEM encoded public key
* @return {Boolean}
*/
let verifySignature = (signature, data, publicKey) => {
return crypto.createVerify('SHA256')
.update(data)
.verify(publicKey, signature);
}
/**
* Returns base64url encoded buffer of the given length
* @param {Number} len - length of the buffer
* @return {String} - base64url random buffer
*/
let randomBase64URLBuffer = (len) => {
len = len || 32;
let buff = crypto.randomBytes(len);
return base64url(buff);
}
/**
* Generates makeCredentials request
* @param {String} username - username
* @param {String} displayName - user's personal display name
* @param {String} id - user's base64url encoded id
* @return {MakePublicKeyCredentialOptions} - server encoded make credentials request
*/
let generateServerMakeCredRequest = (userVerification, requireResidentKey, id, username, displayName) => {
if( userVerification == null) userVerification = userVerificationDefault;
return {
attestation: 'direct',
authenticatorSelection : {
requireResidentKey: requireResidentKey,
userVerification: userVerification
},
challenge: randomBase64URLBuffer(32),
pubKeyCredParams: [
{
type: "public-key", alg: -7 // "ES256" IANA COSE Algorithms registry
}
],
rp: {
//id: "fido.demo.gemalto.com",
name: "Thales FIDO Demo"
},
timeout: 90000,
user: {
id: id,
name: username,
displayName: displayName
}
}
}
/**
* Generates getAssertion request
* @param {Array} authenticators - list of registered authenticators
* @return {PublicKeyCredentialRequestOptions} - server encoded get assertion request
*/
let generateServerGetAssertion = (userVerification, authenticators) => {
if( userVerification == null) userVerification = userVerificationDefault;
let allowCredentials = [];
for(let authr of authenticators) {
allowCredentials.push({
type: 'public-key',
id: authr.credID
//,transports: ['usb', 'nfc', 'ble']
})
}
return {
challenge: randomBase64URLBuffer(32)
,timeout: 60000
,allowCredentials: allowCredentials
,userVerification: userVerification
}
}
/**
* Returns SHA-256 digest of the given data.
* @param {Buffer} data - data to hash
* @return {Buffer} - the hash
*/
let hash = (data) => {
return crypto.createHash('SHA256').update(data).digest();
}
/**
* Takes COSE encoded public key and converts it to RAW PKCS ECDHA key
* @param {Buffer} COSEPublicKey - COSE encoded public key
* @return {Buffer} - RAW PKCS encoded public key
*/
let COSEECDHAtoPKCS = (COSEPublicKey) => {
/*
+------+-------+-------+---------+----------------------------------+
| name | key | label | type | description |
| | type | | | |
+------+-------+-------+---------+----------------------------------+
| crv | 2 | -1 | int / | EC Curve identifier - Taken from |
| | | | tstr | the COSE Curves registry |
| | | | | |
| x | 2 | -2 | bstr | X Coordinate |
| | | | | |
| y | 2 | -3 | bstr / | Y Coordinate |
| | | | bool | |
| | | | | |
| d | 2 | -4 | bstr | Private key |
+------+-------+-------+---------+----------------------------------+
*/
let coseStruct = cbor.decodeAllSync(COSEPublicKey)[0];
let tag = Buffer.from([0x04]);
let x = coseStruct.get(-2);
let y = coseStruct.get(-3);
return Buffer.concat([tag, x, y])
}
/**
* Convert binary certificate or public key to an OpenSSL-compatible PEM text format.
* @param {Buffer} buffer - Cert or PubKey buffer
* @return {String} - PEM
*/
let ASN1toPEM = (pkBuffer) => {
if (!Buffer.isBuffer(pkBuffer))
throw new Error("ASN1toPEM: pkBuffer must be Buffer.")
let type;
if (pkBuffer.length == 65 && pkBuffer[0] == 0x04) {
/*
If needed, we encode rawpublic key to ASN structure, adding metadata:
SEQUENCE {
SEQUENCE {
OBJECTIDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
OBJECTIDENTIFIER 1.2.840.10045.3.1.7 (P-256)
}
BITSTRING <raw public key>
}
Luckily, to do that, we just need to prefix it with constant 26 bytes (metadata is constant).
*/
pkBuffer = Buffer.concat([
new Buffer.from("3059301306072a8648ce3d020106082a8648ce3d030107034200", "hex"),
pkBuffer
]);
type = 'PUBLIC KEY';
} else {
type = 'CERTIFICATE';
}
let b64cert = pkBuffer.toString('base64');
return formatPEM(b64cert, type);
/*
let PEMKey = '';
for(let i = 0; i < Math.ceil(b64cert.length / 64); i++) {
let start = 64 * i;
PEMKey += b64cert.substr(start, 64) + '\n';
}
PEMKey = `-----BEGIN ${type}-----\n` + PEMKey + `-----END ${type}-----\n`;
return PEMKey
*/
}
let formatPEM = (b64cert, type = "CERTIFICATE") => {
let PEMKey = '';
for(let i = 0; i < Math.ceil(b64cert.length / 64); i++) {
let start = 64 * i;
PEMKey += b64cert.substr(start, 64) + '\n';
}
PEMKey = `-----BEGIN ${type}-----\n` + PEMKey + `-----END ${type}-----\n`;
return PEMKey;
}
/**
* Parses authenticatorData buffer.
* @param {Buffer} buffer - authenticatorData buffer
* @return {Object} - parsed authenticatorData struct
*/
let parseMakeCredAuthData = (buffer) => {
let rpIdHash = buffer.slice(0, 32); buffer = buffer.slice(32);
let flagsBuf = buffer.slice(0, 1); buffer = buffer.slice(1);
let flags = flagsBuf[0];
let counterBuf = buffer.slice(0, 4); buffer = buffer.slice(4);
let counter = counterBuf.readUInt32BE(0);
let aaguid = buffer.slice(0, 16); buffer = buffer.slice(16);
let credIDLenBuf = buffer.slice(0, 2); buffer = buffer.slice(2);
let credIDLen = credIDLenBuf.readUInt16BE(0);
let credID = buffer.slice(0, credIDLen); buffer = buffer.slice(credIDLen);
let COSEPublicKey = buffer;
return {rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey}
}
let verifyAuthenticatorAttestationResponse = (webAuthnResponse) => {
console.log("verifyAuthenticatorAttestationResponse");
let attestationBuffer = base64url.toBuffer(webAuthnResponse.response.attestationObject);
let attestationObject = cbor.decodeAllSync(attestationBuffer)[0];
/*
let details = " type: self";
let attestationType = "self";
if( ctapMakeCredResp.attStmt !== undefined && ctapMakeCredResp.attStmt.x5c !== undefined) {
details += " type: AttCA with X5C length: " + ctapMakeCredResp.attStmt.x5c.length;
attestationType = "AttCA";
}
if( ctapMakeCredResp.attStmt !== undefined && ctapMakeCredResp.attStmt.ecdaaKeyId !== undefined) {
details += " type: ECDAA";
attestationType = "ECDAA";
}
console.log("Attestation received: " + ctapMakeCredResp.fmt + " " + details);
*/
console.log("Attestation received: " + attestationObject.fmt);
let authDataBuffer = attestationObject.authData;
attestationObject.authData = parseMakeCredAuthData(authDataBuffer);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let response = {'verified': false, 'fmt': attestationObject.fmt, 'message': "" , 'log' : '', attestationObject: attestationObject };
if(attestationObject.fmt === 'fido-u2f') {
response = u2fAttestation(attestationObject, clientDataHash);
/*
if(!(attestationObject.authData.flags & U2F_USER_PRESENTED))
throw new Error('User was NOT presented durring authentication!');
let reservedByte = Buffer.from([0x00]);
let publicKey = COSEECDHAtoPKCS(attestationObject.authData.COSEPublicKey)
let signatureBase = Buffer.concat([reservedByte, attestationObject.authData.rpIdHash, clientDataHash, attestationObject.authData.credID, publicKey]);
let PEMCertificate = ASN1toPEM(attestationObject.attStmt.x5c[0]);
let signature = attestationObject.attStmt.sig;
// console.log(PEMCertificate);
// console.log(ctapMakeCredResp.attStmt.sig.length);
// console.log(signature.toString("hex"));
// console.log(hash(signature).toString("hex"));
// console.log(ctapMakeCredResp.attStmt.sig.length - 70);
// let signature = Buffer.allocUnsafe(70);
// ctapMakeCredResp.attStmt.sig.copy(signature,0, ctapMakeCredResp.attStmt.sig.length - 70)
let maxTrailingZero = 0;
for(let i = 0; i < signature.length ; i++) {
if( signature.readInt8(i) === 0)
maxTrailingZero = i + 1;
else
break;
}
if( maxTrailingZero > 0) {
console.log("WARNING - I modifiy the length");
signature = signature.slice(maxTrailingZero);
}
console.log(signature.length);
console.log(signature);
// console.log(signature);
// console.log(signatureBase);
// console.log(base64url.decode(webAuthnResponse.response.clientDataJSON));
// console.log(hash(signatureBase).toString("hex"));
// Save cert
let filename = saveCertificate(PEMCertificate);
response.verified = verifySignature(signature, signatureBase, PEMCertificate);
if( !response.verified ) {
console.log("Invalid Signature");
}
// Try to detect device
let cert = Certificate.fromPEM(PEMCertificate);
let aaguid = '';
// The certificate should contain the product ID in this OID
for(let i = 0 ; i < cert.extensions.length ; i ++) {
if( cert.extensions[i].oid === "1.3.6.1.4.1.45724.1.1.4") {
aaguid = cert.extensions[i].value.slice(2);
break;
}
}
// Try to get the first OID
if( aaguid.length <= 0 ) {
for(let i = 0 ; i < cert.extensions.length ; i ++) {
// Transport OID "1.3.6.1.4.1.45724.2.1.1"
if( cert.extensions[i].oid.indexOf("1.3.6.1.4.1.") == 0 && cert.extensions[i].oid.indexOf("1.3.6.1.4.1.45724.2.1.1") !== 0 ) {
aaguid = cert.extensions[i].value;
break;
}
}
}
if(response.verified) {
console.log("Attestation Verified");
response.attestationObject.attStmt.sig = response.attestationObject.attStmt.sig.toString('base64');
response.authrInfo = {
fmt: 'fido-u2f',
aaguid: convertAAGUID(aaguid),
publicKey: base64url.encode(publicKey),
counter: attestationObject.authData.counter,
credID: base64url.encode(attestationObject.authData.credID),
cert: filename
}
}
else
console.log("Attestation NOT Verified");
*/
}
else if (attestationObject.fmt === 'packed' && attestationObject.attStmt.x5c !== undefined) {
response = packedAttestation(attestationObject, clientDataHash, authDataBuffer);
/*
// https://www.w3.org/TR/webauthn/#packed-attestation
let authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
// Step 1 - Verify that sig is a valid signature
let signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
let PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
let signature = ctapMakeCredResp.attStmt.sig;
response.verified = verifySignature(signature, signatureBase, PEMCertificate);
if( !response.verified ) response.message = "Invalid Signature";
// Save cert
let filename = "cert/" + crypto.createHash('md5').update(authrDataStruct.credID).digest("hex") + ".crt";
fs.writeFileSync( "static/" + filename, PEMCertificate);
// Step 2 - ???
// Step 3 - if OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid)
// verify that the value of this extension matches the aaguid in authenticatorData.
// DEBUG: PEMCertificate = fs.readFileSync("cert/card.crt");
//PEMCertificate = fs.readFileSync("cert/ctap_fpvcFk2ZxU8RsZDihqbO23ARBn4sKK_Y0akjna8tAhIt2vfCWgw29_F5KrWFaAb4-PEjYjW_lqPMgccDmwrEVu6CaEQsEaektvPLiig.crt");
let checkAAGUID = true;
if( checkAAGUID )
{
let cert = Certificate.fromPEM(PEMCertificate);
for(let i = 0 ; i < cert.extensions.length ; i ++) {
if( cert.extensions[i].oid === "1.3.6.1.4.1.45724.1.1.4") {
let certValue = Buffer.from(cert.extensions[i].value, 2);
response.verified = certValue.equals(authrDataStruct.aaguid);
if( !response.verified ) {
response.message = "Invalid AAGUID";
response.log = "Invalid AAGUID [" + JSON.stringify(authrDataStruct.aaguid) + "] [" + JSON.stringify(certValue) + "]";
console.log(response.log);
}
break;
}
}
}
if(response.verified) {
response.authrInfo = {
fmt: 'packed',
aaguid: authrDataStruct.aaguid.toString('hex'),
publicKey: base64url.encode(publicKey),
counter: authrDataStruct.counter,
credID: base64url.encode(authrDataStruct.credID),
cert: filename
}
}
*/
}
/*
else if (ctapMakeCredResp.fmt === "android-key")
{
let authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
// Step 1 - Verify that sig is a valid signature
let signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
let PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
let signature = ctapMakeCredResp.attStmt.sig;
response.verified = verifySignature(signature, signatureBase, PEMCertificate);
if( !response.verified ) response.message = "Invalid Signature";
// Save cert
let filename = "cert/" + crypto.createHash('md5').update(authrDataStruct.credID).digest("hex") + ".crt";
fs.writeFileSync( "static/" + filename, PEMCertificate);
if(response.verified) {
response.authrInfo = {
fmt: ctapMakeCredResp.fmt,
aaguid: authrDataStruct.aaguid.toString('hex'),
publicKey: base64url.encode(publicKey),
counter: authrDataStruct.counter,
credID: base64url.encode(authrDataStruct.credID),
cert: filename
}
}
}
*/
else if (ctapMakeCredResp.fmt === "android-safetynet")
{
response = androidSafetynetAttestation(attestationObject, clientDataHash);
/*
let authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let publicKey = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey);
let ver = ctapMakeCredResp.attStmt.ver;
let response = ctapMakeCredResp.attStmt.response.toString("utf-8");
console.log(ver);
let jwsArray = response.split(".");
if( jwsArray.length )
let nonce = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]).toString('base64');
*/
/*
// Step 1 - Verify that sig is a valid signature
let signatureBase = Buffer.concat([ctapMakeCredResp.authData, clientDataHash]);
let PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
let signature = ctapMakeCredResp.attStmt.sig;
response.verified = verifySignature(signature, signatureBase, PEMCertificate);
if( !response.verified ) response.message = "Invalid Signature";
// Save cert
let filename = "cert/" + crypto.createHash('md5').update(authrDataStruct.credID).digest("hex") + ".crt";
fs.writeFileSync( "static/" + filename, PEMCertificate);
if(response.verified) {
response.authrInfo = {
fmt: ctapMakeCredResp.fmt,
aaguid: authrDataStruct.aaguid.toString('hex'),
publicKey: base64url.encode(publicKey),
counter: authrDataStruct.counter,
credID: base64url.encode(authrDataStruct.credID),
cert: filename
}
}
*/
}
else
{
response.authrInfo = {
fmt: attestationObject.fmt,
}
response.message = 'Unsupported attestation [' + attestationObject.fmt + "]";
}
// Format attestationObject for logs
if( response.verified == true)
{
attestationLog = {attStmt: {x5c: []}, authData: {credentialData: {}}};
// Prepare "attStmt"
attestationLog.attStmt.sig = attestationObject.attStmt.sig.toString('base64');
attestationObject.attStmt.x5c.forEach(x5c => attestationLog.attStmt.x5c.push(x5c.toString('base64')));
// Prepare "credentialData"
attestationLog.authData.credentialData.aaguid = attestationObject.authData.aaguid.toString('base64');
attestationLog.authData.credentialData.credentialId = attestationObject.authData.credID.toString('base64');
attestationLog.authData.credentialData.rpIdHash = attestationObject.authData.rpIdHash.toString('base64');
attestationLog.authData.credentialData.signatureCounter = attestationObject.authData.signatureCounter;
// Prepare "fmt"
attestationLog.fmt = attestationObject.fmt;
// Assign the log object
response.attestationObject = attestationLog;
/*
"credentialData": {
"aaguid": "AAAAAAAAAAAAAAAAAAAAAA==",
"credentialId": "aRaHCZ8z63X946K6WFwE5+Naqcc0P3mw46/23s4dHMc5xmjuAmVav4wiAl1LjHlimW2ABKYFl4govGNffdNrktNiU6xr8qNSCh+mqP5MJI6DRq4Z65o5QkABkG1ElcZXsO83ACCFL9JAeRj9X9ufqG4qC31v91Sjk9v2gunfnrcda6fRtNBA9yn9/ONoxbzuXg5LeGV7MRM6NNUwrCREYQ==",
"publicKey": {
"1": 2,
"3": -7,
"-1": 1,
"-2": "ia56X+doqE2bb+rmkT5gM6jQZe2zfb6xAHR55uM6Lyc=",
"-3": "fWnMFdHhu9An/oguPJLHTarRHdFCjGTEiO9lOW8hJmE="
}
},
"flags": {
"AT": true,
"ED": false,
"UP": true,
"UV": false,
"value": 65
},
"rpIdHash": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7c=",
"signatureCounter": 0 },
"fmt": "fido-u2f"
*/
}
return response
}
/**
* save cert
* @param {Buffer} data - certificate Data
* @return {String} - filename
*/
let saveCertificate = (data) => {
let filename = "cert/" + crypto.createHash('md5').update(data).digest("hex") + ".crt";
fs.writeFileSync( "static/" + filename, data);
return filename;
}
function u2fAttestation(attestationObject, clientDataHash) {
if(!(attestationObject.authData.flags & U2F_USER_PRESENTED)) {
console.log("User presented FLAG");
return {verified: false, message: "User presented FLAG"};
}
let reservedByte = Buffer.from([0x00]);
let publicKey = COSEECDHAtoPKCS(attestationObject.authData.COSEPublicKey)
let signatureBase = Buffer.concat([reservedByte, attestationObject.authData.rpIdHash, clientDataHash, attestationObject.authData.credID, publicKey]);
let PEMCertificate = ASN1toPEM(attestationObject.attStmt.x5c[0]);
let signature = attestationObject.attStmt.sig;
let maxTrailingZero = 0;
for(let i = 0; i < signature.length ; i++) {
if( signature.readInt8(i) === 0)
maxTrailingZero = i + 1;
else
break;
}
if( maxTrailingZero > 0) {
console.log("WARNING - I modify the length");
signature = signature.slice(maxTrailingZero);
}
// Save cert
let filename = saveCertificate(PEMCertificate);
if( !verifySignature(signature, signatureBase, PEMCertificate) ) {
console.log("Invalid Signature");
return {verified: false, message: "Invalid Signature"};
}
// Try to detect device
let cert = Certificate.fromPEM(PEMCertificate);
let aaguid = '';
// The certificate should contain the product ID in this OID
for(let i = 0 ; i < cert.extensions.length ; i ++) {
if( cert.extensions[i].oid === "1.3.6.1.4.1.45724.1.1.4") {
aaguid = cert.extensions[i].value.slice(2);
break;
}
}
// Try to get the first OID
if( aaguid.length <= 0 ) {
for(let i = 0 ; i < cert.extensions.length ; i ++) {
// Transport OID "1.3.6.1.4.1.45724.2.1.1"
if( cert.extensions[i].oid.indexOf("1.3.6.1.4.1.") == 0 && cert.extensions[i].oid.indexOf("1.3.6.1.4.1.45724.2.1.1") !== 0 ) {
aaguid = cert.extensions[i].value;
break;
}
}
}
console.log("U2F Attestation Verified");
let authrInfo = {
fmt: 'fido-u2f',
aaguid: convertAAGUID(aaguid),
publicKey: base64url.encode(publicKey),
counter: attestationObject.authData.counter,
credID: base64url.encode(attestationObject.authData.credID),
cert: filename };
return {verified: true, authrInfo: authrInfo, message: "OK", attestationObject: attestationObject};
}
/**
* Validate packed attestation
* @param {Buffer} attestationObject - ctapMakeCred buffer
* @param {String} clientDataHash - hash
* @param {String} authDataBuffer - Original buffer
* @return {Object} - parsed authenticatorData struct
*/
let packedAttestation = (attestationObject, clientDataHash, authDataBuffer) => {
// https://www.w3.org/TR/webauthn/#packed-attestation
let publicKey = COSEECDHAtoPKCS(attestationObject.authData.COSEPublicKey);
// Step 1 - Verify that sig is a valid signature
let signatureBase = Buffer.concat([authDataBuffer, clientDataHash]);
let PEMCertificate = ASN1toPEM(attestationObject.attStmt.x5c[0]);
let signature = attestationObject.attStmt.sig;
if( !verifySignature(signature, signatureBase, PEMCertificate) )
{
console.log("invalid Signature");
return {verified: false, message: "invalid Signature"};
}
// Save cert
let filename = saveCertificate(PEMCertificate);
// Step 2 - ???
// Step 3 - if OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid)
// verify that the value of this extension matches the aaguid in authenticatorData.
// DEBUG: PEMCertificate = fs.readFileSync("cert/card.crt");
//PEMCertificate = fs.readFileSync("cert/ctap_fpvcFk2ZxU8RsZDihqbO23ARBn4sKK_Y0akjna8tAhIt2vfCWgw29_F5KrWFaAb4-PEjYjW_lqPMgccDmwrEVu6CaEQsEaektvPLiig.crt");
let checkAAGUID = true;
if( checkAAGUID )
{
let cert = Certificate.fromPEM(PEMCertificate);
for(let i = 0 ; i < cert.extensions.length ; i ++) {
if( cert.extensions[i].oid === "1.3.6.1.4.1.45724.1.1.4") {
let certValue = cert.extensions[i].value.slice(2);
if( !certValue.equals(attestationObject.authData.aaguid)) {
let log = "Invalid AAGUID [" + JSON.stringify(attestationObject.authData.aaguid) + "] [" + JSON.stringify(certValue) + "]";
console.log(log);
return {verified: false, message: "invalid AAGUID", log: log};
}
break;
}
}
}
let authrInfo = {
fmt: attestationObject.fmt,
aaguid: convertAAGUID(attestationObject.authData.aaguid),
publicKey: base64url.encode(publicKey),
counter: attestationObject.authData.counter,
credID: base64url.encode(attestationObject.authData.credID),
cert: filename
};
console.log("Attestation is valid");
return {verified: true, authrInfo: authrInfo, message: "OK", attestationObject: attestationObject};
}
let convertAAGUID = (byteArray) => {
if( byteArray.length == 18 )
byteArray = byteArray.slice(2);
let aaguid = byteArray.toString('hex').toLowerCase();
aaguid = aaguid.slice(0,8) + '-' + aaguid.slice(8);
aaguid = aaguid.slice(0,13) + '-' + aaguid.slice(13);
aaguid = aaguid.slice(0,18) + '-' + aaguid.slice(18);
aaguid = aaguid.slice(0,23) + '-' + aaguid.slice(23);
return aaguid;
}
/**
* Validate android-safetynet attestation
* @param {Buffer} attestationObject - ctapMakeCred buffer
* @param {String} clientDataHash - hash
* @return {Object} - parsed authenticatorData struct
*/
let androidSafetynetAttestation = (attestationObject, clientDataHash) => {
//let authrDataStruct = parseMakeCredAuthData(attestationObject.authData);
let publicKey = COSEECDHAtoPKCS(attestationObject.authData.COSEPublicKey);
let ver = attestationObject.attStmt.ver;
let response = attestationObject.attStmt.response.toString("utf-8");
let jwsArray = response.split(".");
if( jwsArray.length <= 2 )
return {verified: false, message: "invalid JWS attestation"}
// STEP 1 - Get certificate
let attestation = JSON.parse(base64url.decode(jwsArray[0]));
let PEMCertificate = formatPEM(attestation.x5c[0]);
// Save cert
let filename = saveCertificate(PEMCertificate);
// Compare hostname
let cert = Certificate.fromPEM(PEMCertificate);
let hostnameIsValid = false;
for(let i = 0 ; i < cert.subject.attributes.length ; i++) {
if(( cert.subject.attributes[i].shortName === 'CN' ) && (cert.subject.attributes[i].value === 'attest.android.com'))
hostnameIsValid = true;
}
if( !hostnameIsValid )
return {verified: false, message: "invalid Hostname"}
// STEP 2 - Verify nonce
let jws = JSON.parse(base64url.decode(jwsArray[1]));
let nonce = crypto.createHash('sha256').update(Buffer.concat([attestationObject.authData, clientDataHash])).digest().toString('base64');
if( nonce != jws.nonce)
return {verified: false, message: "invalid nonce"}
// STEP 2 - Verify ctsProfileMatch = true
if( jws.ctsProfileMatch !== true)
return {verified: false, message: "invalid ctsProfileMatch"}
let authrInfo = {
fmt: attestationObject.fmt,
aaguid: attestationObject.authData.aaguid.toString('hex'),
publicKey: base64url.encode(publicKey),
counter: attestationObject.authData.counter,
credID: base64url.encode(attestationObject.authData.credID),
cert: filename
};
console.log("Attestation is valid");
return {verified: true, authrInfo: authrInfo, message: "OK", attestationObject: attestationObject};
}
/**
* Takes an array of registered authenticators and find one specified by credID
* @param {String} credID - base64url encoded credential
* @param {Array} authenticators - list of authenticators
* @return {Object} - found authenticator
*/
let findAuthr = (credID, authenticators) => {
for(let authr of authenticators) {
if(authr.credID === credID)
return authr
}
throw new Error(`Unknown authenticator with credID ${credID}!`)
}
/**
* Parses AuthenticatorData from GetAssertion response
* @param {Buffer} buffer - Auth data buffer
* @return {Object} - parsed authenticatorData struct
*/
let parseGetAssertAuthData = (buffer) => {
let rpIdHash = buffer.slice(0, 32); buffer = buffer.slice(32);
let flagsBuf = buffer.slice(0, 1); buffer = buffer.slice(1);
let flags = flagsBuf[0];
let counterBuf = buffer.slice(0, 4); buffer = buffer.slice(4);
let counter = counterBuf.readUInt32BE(0);
return {rpIdHash, flagsBuf, flags, counter, counterBuf}
}
let verifyAuthenticatorAssertionResponse = (webAuthnResponse, authenticators) => {
let authr = findAuthr(webAuthnResponse.id, authenticators);
let authenticatorData = base64url.toBuffer(webAuthnResponse.response.authenticatorData);
let response = {'verified': false};
//*********************************************** */
// Generic check
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON));
if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
throw new Error('User was NOT presented durring authentication!');
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
let signatureBase = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
response.verified = verifySignature(signature, signatureBase, publicKey);
if( !response.verified ) response.message = "Invalid Signature";
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
/*
if(authr.fmt === 'fido-u2f') {
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
throw new Error('User was NOT presented durring authentication!');
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let signatureBase = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
response.verified = verifySignature(signature, signatureBase, publicKey)
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
}
else if(authr.fmt === 'packed') {
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
let signatureBase = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
// Step 1 - Verify that sig is a valid signature
response.verified = verifySignature(signature, signatureBase, publicKey);
if( !response.verified ) response.message = "Invalid Signature";
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
}
else if(authr.fmt === 'android-safetynet') {
console.log("login with: " + authr.fmt);
let authrDataStruct = parseGetAssertAuthData(authenticatorData);
let clientDataHash = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON));
let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
let signature = base64url.toBuffer(webAuthnResponse.response.signature);
let signatureBase = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);
console.log(publicKey);
console.log(signature);
console.log(signatureBase);
// Step 1 - Verify that sig is a valid signature
response.verified = verifySignature(signature, signatureBase, publicKey);
if( !response.verified ) response.message = "Invalid Signature";
console.log(authr);
console.log(authrDataStruct);
console.log(clientDataHash);
//response.verified = true;
if(response.verified) {
if(response.counter <= authr.counter)
throw new Error('Authr counter did not increase!');
authr.counter = authrDataStruct.counter
}
}*/
return response;
}
module.exports = {
randomBase64URLBuffer,
generateServerMakeCredRequest,
generateServerGetAssertion,
verifyAuthenticatorAttestationResponse,
verifyAuthenticatorAssertionResponse
}
Accelerate Your Automation Test Cycles With LambdaTest
Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.