Files
PageSigner/core/oracles.js
2022-03-29 23:22:51 +03:00

347 lines
17 KiB
JavaScript

import {ba2str, b64decode, assert, ba2int, verifyAttestationDoc, ba2hex, eq,
sha256} from './utils.js';
// rootsOfTrust contains an array of trusted EBS snapshots
// essentially the whole oracle verification procedure boils down to proving that an EC2 instance was
// launched from an AMI which was created from one of the "rootsOfTrust" snapshot ids.
const rootsOfTrust = [
'snap-0ccb00d0e0fb4d4da',
'snap-07eda3ed4836f82fb',
'snap-023d50ee97873a1f0',
'snap-0e50af508006037dc',
'snap-023dda76582a6b29f',
'snap-027ade4f1002864da'];
// URLFetcher trusted enclave measurements, see
// https://github.com/tlsnotary/URLFetcher
const URLFetcherPCR0 = 'f70217239e8a1cb0f3c010b842a279e2b8d30d3700d7e4722fef22291763479a13783dc76d5219fabbd7e5aa92a7b255';
const URLFetcherPCR1 = 'c35e620586e91ed40ca5ce360eedf77ba673719135951e293121cb3931220b00f87b5a15e94e25c01fecd08fc9139342';
const URLFetcherPCR2 = 'efba114128ccd6af1d1366a12c1ac89e4a4ca5ea1434d779efadfd3ec0d1da5b7c0d8525239fac29ffde2946e07d1c16';
// assuming both events happened on the same day, get the time
// difference between them in seconds
// the time string looks like "2015-04-15T19:00:59.000Z"
function getSecondsDelta(later, sooner) {
assert(later.length == 24);
if (later.slice(0, 11) !== sooner.slice(0, 11)) {
return 999999; // not on the same day
}
const laterTime = later.slice(11, 19).split(':');
const soonerTime = sooner.slice(11, 19).split(':');
const laterSecs = parseInt(laterTime[0]) * 3600 + parseInt(laterTime[1]) * 60 + parseInt(laterTime[2]);
const soonerSecs = parseInt(soonerTime[0]) * 3600 + parseInt(soonerTime[1]) * 60 + parseInt(soonerTime[2]);
return laterSecs - soonerSecs;
}
function checkDescribeInstances(xmlDoc, instanceId, imageId, volumeId) {
try {
assert(xmlDoc.getElementsByTagName('DescribeInstancesResponse').length == 1);
const rs = xmlDoc.getElementsByTagName('reservationSet');
assert(rs.length === 1);
const rs_items = rs[0].children;
assert(rs_items.length === 1);
var ownerId = rs_items[0].getElementsByTagName('ownerId')[0].textContent;
const isets = rs_items[0].getElementsByTagName('instancesSet');
assert(isets.length === 1);
const instances = isets[0].children;
assert(instances.length === 1);
const parent = instances[0];
assert(parent.getElementsByTagName('instanceId')[0].textContent === instanceId);
assert(parent.getElementsByTagName('imageId')[0].textContent === imageId);
assert(parent.getElementsByTagName('instanceState')[0].getElementsByTagName('name')[0].textContent === 'running');
// other instance types may use non-nvme disks and thus would bypass the check that
// only one nvme* disk is allowed
assert (parent.getElementsByTagName('instanceType')[0].textContent.startsWith('t3'));
var launchTime = parent.getElementsByTagName('launchTime')[0].textContent;
assert(parent.getElementsByTagName('rootDeviceType')[0].textContent === 'ebs');
assert(parent.getElementsByTagName('rootDeviceName')[0].textContent === '/dev/sda1');
const devices = parent.getElementsByTagName('blockDeviceMapping')[0].getElementsByTagName('item');
assert(devices.length === 1);
assert(devices[0].getElementsByTagName('deviceName')[0].textContent === '/dev/sda1');
assert(devices[0].getElementsByTagName('ebs')[0].getElementsByTagName('status')[0].textContent === 'attached');
var volAttachTime = devices[0].getElementsByTagName('ebs')[0].getElementsByTagName('attachTime')[0].textContent;
assert(devices[0].getElementsByTagName('ebs')[0].getElementsByTagName('volumeId')[0].textContent === volumeId);
// get seconds from "2015-04-15T19:00:59.000Z"
assert(getSecondsDelta(volAttachTime, launchTime) <= 2);
assert(parent.getElementsByTagName('virtualizationType')[0].textContent === 'hvm');
assert(parent.getElementsByTagName('hypervisor')[0].textContent === 'xen');
} catch (e) {
throw('checkDescribeInstances exception');
}
return {
'ownerId': ownerId,
'volAttachTime': volAttachTime,
'launchTime': launchTime
};
}
function checkDescribeVolumes(xmlDoc, instanceId, volumeId, volAttachTime, snapshotIds) {
try {
assert(xmlDoc.getElementsByTagName('DescribeVolumesResponse').length == 1);
const volumes = xmlDoc.getElementsByTagName('volumeSet')[0].children;
assert(volumes.length === 1);
const volume = volumes[0];
assert(volume.getElementsByTagName('volumeId')[0].textContent === volumeId);
assert(snapshotIds.includes(volume.getElementsByTagName('snapshotId')[0].textContent));
assert(volume.getElementsByTagName('status')[0].textContent === 'in-use');
const volCreateTime = volume.getElementsByTagName('createTime')[0].textContent;
const attVolumes = volume.getElementsByTagName('attachmentSet')[0].getElementsByTagName('item');
assert(attVolumes.length === 1);
const attVolume = attVolumes[0];
assert(attVolume.getElementsByTagName('volumeId')[0].textContent === volumeId);
assert(attVolume.getElementsByTagName('instanceId')[0].textContent === instanceId);
assert(attVolume.getElementsByTagName('device')[0].textContent === '/dev/sda1');
assert(attVolume.getElementsByTagName('status')[0].textContent === 'attached');
const attTime = attVolume.getElementsByTagName('attachTime')[0].textContent;
assert(volAttachTime === attTime);
// Crucial: volume was created from snapshot and attached at the same instant
// this guarantees that there was no time window to modify it
assert(getSecondsDelta(attTime, volCreateTime) === 0);
} catch (e) {
throw('checkDescribeVolumes exception');
}
return true;
}
function checkGetConsoleOutput(xmlDoc, instanceId) {
try {
assert(xmlDoc.getElementsByTagName('GetConsoleOutputResponse').length == 1);
assert(xmlDoc.getElementsByTagName('instanceId')[0].textContent === instanceId);
const b64data = xmlDoc.getElementsByTagName('output')[0].textContent;
const logstr = ba2str(b64decode(b64data));
// the only nvme* strings allowed are: nvme, nvme0, nvme0n1, nvme0n1p1
// this ensures that instance has only one disk device. This is
// a redundant check, because the patched ramdisk must halt the boot process if
// it detects more than one disk device.
const allowedSet = ['nvme', 'nvme0', 'nvme0n1', 'nvme0n1p1'];
// match all substrings starting with nvme, folowed by a count of from 0 to 7 symbols from
// the ranges 0-9 and a-z
for (const match of [...logstr.matchAll(/nvme[0-9a-z]{0,7}/g)]){
assert(match.length == 1);
assert(allowedSet.includes(match[0]), 'disallowed nvme* string present in log');
}
const sigmark = 'PageSigner public key for verification';
const pkstartmark = '-----BEGIN PUBLIC KEY-----';
const pkendmark = '-----END PUBLIC KEY-----';
const mark_start = logstr.search(sigmark);
assert(mark_start !== -1);
const pubkey_start = mark_start + logstr.slice(mark_start).search(pkstartmark);
const pubkey_end = pubkey_start + logstr.slice(pubkey_start).search(pkendmark) + pkendmark.length;
const chunk = logstr.slice(pubkey_start, pubkey_end);
const lines = chunk.split('\n');
let pk = pkstartmark + '\n';
for (let i = 1; i < lines.length-1; i++) {
const words = lines[i].split(' ');
pk = pk + words[words.length-1] + '\n';
}
pk = pk + pkendmark;
assert(pk.length > 0);
const pubkeyPEM = pk.split('\r\n').join('\n');
return pubkeyPEM;
} catch (e) {
throw('checkGetConsoleOutput exception');
}
}
// "userData" allows to pass an arbitrary script to the instance at launch. It MUST be empty.
// This is a sanity check because the instance is stripped of the code which parses userData.
function checkDescribeInstanceAttributeUserdata(xmlDoc, instanceId) {
try {
assert(xmlDoc.getElementsByTagName('DescribeInstanceAttributeResponse').length == 1);
assert(xmlDoc.getElementsByTagName('instanceId')[0].textContent === instanceId);
assert(xmlDoc.getElementsByTagName('userData')[0].textContent === '');
} catch (e) {
throw('checkDescribeInstanceAttributeUserdata exception');
}
return true;
}
function checkDescribeInstanceAttributeKernel(xmlDoc, instanceId) {
try {
assert(xmlDoc.getElementsByTagName('DescribeInstanceAttributeResponse').length == 1);
assert(xmlDoc.getElementsByTagName('instanceId')[0].textContent === instanceId);
assert(xmlDoc.getElementsByTagName('kernel')[0].textContent === '');
} catch (e) {
throw('checkDescribeInstanceAttributeKernel exception');
}
return true;
}
function checkDescribeInstanceAttributeRamdisk(xmlDoc, instanceId) {
try {
assert(xmlDoc.getElementsByTagName('DescribeInstanceAttributeResponse').length == 1);
assert(xmlDoc.getElementsByTagName('instanceId')[0].textContent === instanceId);
assert(xmlDoc.getElementsByTagName('ramdisk')[0].textContent === '');
} catch (e) {
throw('checkDescribeInstanceAttributeRamdisk exception');
}
return true;
}
function checkGetUser(xmlDoc, ownerId) {
try {
assert(xmlDoc.getElementsByTagName('GetUserResponse').length == 1);
assert(xmlDoc.getElementsByTagName('UserId')[0].textContent === ownerId);
assert(xmlDoc.getElementsByTagName('Arn')[0].textContent.slice(-(ownerId.length + ':root'.length)) === ownerId + ':root');
} catch (e) {
throw('checkGetUser exception');
}
return true;
}
function checkDescribeImages(xmlDoc, imageId, snapshotIds){
try {
assert(xmlDoc.getElementsByTagName('DescribeImagesResponse').length == 1);
const images = xmlDoc.getElementsByTagName('imagesSet')[0].children;
assert(images.length == 1);
const image = images[0];
assert(image.getElementsByTagName('imageId')[0].textContent == imageId);
assert(image.getElementsByTagName('imageState')[0].textContent == 'available');
assert(image.getElementsByTagName('rootDeviceName')[0].textContent == '/dev/sda1');
const devices = image.getElementsByTagName('blockDeviceMapping')[0].children;
assert(devices.length == 1);
const device = devices[0];
assert(device.getElementsByTagName('deviceName')[0].textContent == '/dev/sda1');
const ebs = device.getElementsByTagName('ebs')[0];
assert(snapshotIds.includes(ebs.getElementsByTagName('snapshotId')[0].textContent));
assert(image.getElementsByTagName('virtualizationType')[0].textContent == 'hvm');
assert(image.getElementsByTagName('hypervisor')[0].textContent == 'xen');
} catch (e) {
throw('checkDescribeImages exception');
}
return true;
}
async function fetch_and_parse(obj){
// we don't fetch it ourselves anymore. URLFetcher already did that for us.
// const req = await fetch(obj.request);
// const text = await req.text();
const xmlDoc = new DOMParser().parseFromString(obj.response, 'text/xml');
return xmlDoc;
}
export async function getURLFetcherDoc(IP, port = 10011){
// get URLFetcher document containing attestation for AWS HTTP API URLs needed to
// verify that the oracle was correctly set up.
// https://github.com/tlsnotary/URLFetcher
const resp = await fetch('http://' + IP + ':' + port + '/getURLFetcherDoc', {
method: 'POST',
mode: 'cors',
cache: 'no-store',
});
return new Uint8Array(await resp.arrayBuffer());
}
export async function verifyNotary(URLFetcherDoc) {
// URLFetcherDoc is a concatenation of 4-byte transcript length | transcript | attestation doc
const transcriptLen = ba2int(URLFetcherDoc.slice(0, 4));
const transcript = URLFetcherDoc.slice(4, 4+transcriptLen);
const attestation = URLFetcherDoc.slice(4+transcriptLen);
// transcript is a JSON array for each request[ {"request":<URL>, "response":<text>} , {...}]
const transJSON = JSON.parse(ba2str(transcript));
// find which URL corresponds to which API call
const markers = [
{'DescribeInstances': 'DI'},
{'DescribeVolumes': 'DV'},
{'GetConsoleOutput': 'GCO'},
{'GetUser': 'GU'},
{'userData': 'DIAud'},
{'kernel': 'DIAk'},
{'ramdisk': 'DIAr'},
{'DescribeImages': 'DImg'}];
const o = {};
for (let i=0; i < markers.length; i++){
const key = Object.keys(markers[i]);
for (let j=0; j < transJSON.length; j++){
if (transJSON[j].request.indexOf(key) > -1){
o[markers[i][key]] = transJSON[j];
}
}
}
assert(Object.keys(o).length === markers.length);
// check that the URLs are formatted in a canonical way
// Note that AWS expects URL params to be sorted alphabetically. If we put them in
// arbitrary order, the query will be rejected
// "AWSAccessKeyId" should be the same in all URLs to prove that the queries are made
// on behalf of AWS user "root". Otherwise, a potential attack opens up when AWS APi calls
// are made on behalf of a user with limited privileges for whom the API report only
// partial information.
const AWSAccessKeyId = o.DI.request.match(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId=[A-Z0-9]{20}'))[0].split('=')[1];
// We only allow oracles instantiated from TLSNotary's AWS account.
assert(AWSAccessKeyId === 'AKIAI2NJVYXCCAQDCC5Q');
assert(AWSAccessKeyId.length === 20);
const instanceId = o.DI.request.match(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeInstances&Expires=2030-01-01&InstanceId=i-[a-f0-9]{17}'))[0].split('=')[4];
assert(instanceId.length === 19);
const volumeId = o.DV.request.match(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeVolumes&Expires=2030-01-01&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&VolumeId=vol-[a-f0-9]{17}'))[0].split('=')[7];
assert(volumeId.length === 21);
const amiId = o.DImg.request.match(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeImages&Expires=2030-01-01&ImageId.1=ami-[a-f0-9]{17}'))[0].split('=')[4];
assert(amiId.length === 21);
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeInstances&Expires=2030-01-01&InstanceId='+instanceId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DI.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeVolumes&Expires=2030-01-01&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&VolumeId='+volumeId+'&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DV.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=GetConsoleOutput&Expires=2030-01-01&InstanceId='+instanceId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.GCO.request));
assert(new RegExp('^https://iam.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=GetUser&Expires=2030-01-01&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2010-05-08&Signature=[a-zA-Z0-9%]{46,56}$').test(o.GU.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeInstanceAttribute&Attribute=userData&Expires=2030-01-01&InstanceId='+instanceId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DIAud.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeInstanceAttribute&Attribute=kernel&Expires=2030-01-01&InstanceId='+instanceId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DIAk.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeInstanceAttribute&Attribute=ramdisk&Expires=2030-01-01&InstanceId='+instanceId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DIAr.request));
assert(new RegExp('^https://ec2.us-east-1.amazonaws.com/\\?AWSAccessKeyId='+AWSAccessKeyId+'&Action=DescribeImages&Expires=2030-01-01&ImageId.1='+amiId+'&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2014-10-01&Signature=[a-zA-Z0-9%]{46,56}$').test(o.DImg.request));
const xmlDocDI = await fetch_and_parse(o.DI);
const rv = checkDescribeInstances(xmlDocDI, instanceId, amiId, volumeId);
const volAttachTime = rv.volAttachTime;
const ownerId = rv.ownerId;
const xmlDocDV = await fetch_and_parse(o.DV);
checkDescribeVolumes(xmlDocDV, instanceId, volumeId, volAttachTime, rootsOfTrust);
const xmlDocGU = await fetch_and_parse(o.GU);
checkGetUser(xmlDocGU, ownerId);
const xmlDocGCO = await fetch_and_parse(o.GCO);
const pubkeyPEM = checkGetConsoleOutput(xmlDocGCO, instanceId);
const xmlDocDIAud = await fetch_and_parse(o.DIAud);
checkDescribeInstanceAttributeUserdata(xmlDocDIAud, instanceId);
const xmlDocDIAk = await fetch_and_parse(o.DIAk);
checkDescribeInstanceAttributeKernel(xmlDocDIAk, instanceId);
const xmlDocDIAr = await fetch_and_parse(o.DIAr);
checkDescribeInstanceAttributeRamdisk(xmlDocDIAr, instanceId);
const xmlDocDImg = await fetch_and_parse(o.DImg);
checkDescribeImages(xmlDocDImg, amiId, rootsOfTrust);
// verify the attestation document
const attestRV = await verifyAttestationDoc(attestation);
assert(eq(attestRV[0], await sha256(transcript)));
assert(URLFetcherPCR0 === ba2hex(attestRV[1]));
assert(URLFetcherPCR1 === ba2hex(attestRV[2]));
assert(URLFetcherPCR2 === ba2hex(attestRV[3]));
console.log('oracle verification successfully finished');
return pubkeyPEM;
}