https://github.com/quicwg/base-drafts
Tip revision: a16ccf150f9a6bf006d5d5206c76d9e30468e743 authored by Mike Bishop on 20 May 2020, 00:15:42 UTC
Core terminology in QPACK (#3520)
Core terminology in QPACK (#3520)
Tip revision: a16ccf1
protection-samples.js
#!/bin/sh
':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
// This script performs simple encryption and decryption for Initial packets.
// It's crude, but it should be sufficient to generate examples.
'use strict';
require('buffer');
var crypto = require('crypto');
var INITIAL_SALT = Buffer.from('c3eef712c72ebb5a11a7d2432bb46365bef9f502', 'hex');
var SHA256 = 'sha256';
var AES_GCM = 'aes-128-gcm';
var AES_ECB = 'aes-128-ecb';
const draft_version = 28;
var version = 'ff0000' + draft_version.toString(16);
function chunk(s, n) {
return (new Array(Math.ceil(s.length / n)))
.fill()
.map((_, i) => s.slice(i * n, i * n + n));
}
function log(m, k) {
console.log(m + ' [' + k.length + ']: ' + chunk(k.toString('hex'), 32).join(' '));
};
class HMAC {
constructor(hash) {
this.hash = hash;
}
digest(key, input) {
var hmac = crypto.createHmac(this.hash, key);
hmac.update(input);
return hmac.digest();
}
}
/* HKDF as defined in RFC5869, with HKDF-Expand-Label from RFC8446. */
class QHKDF {
constructor(hmac, prk) {
this.hmac = hmac;
this.prk = prk;
}
static extract(hash, salt, ikm) {
var hmac = new HMAC(hash);
return new QHKDF(hmac, hmac.digest(salt, ikm));
}
expand(info, len) {
var output = Buffer.alloc(0);
var T = Buffer.alloc(0);
info = Buffer.from(info, 'ascii');
var counter = 0;
var cbuf = Buffer.alloc(1);
while (output.length < len) {
cbuf.writeUIntBE(++counter, 0, 1);
T = this.hmac.digest(this.prk, Buffer.concat([T, info, cbuf]));
output = Buffer.concat([output, T]);
}
return output.slice(0, len);
}
expand_label(label, len) {
const prefix = "tls13 ";
var info = Buffer.alloc(2 + 1 + prefix.length + label.length + 1);
// Note that Buffer.write returns the number of bytes written, whereas
// Buffer.writeUIntBE returns the end offset of the write. Consistency FTW.
var offset = info.writeUIntBE(len, 0, 2);
offset = info.writeUIntBE(prefix.length + label.length, offset, 1);
offset += info.write(prefix + label, offset);
info.writeUIntBE(0, offset, 1);
log('info for ' + label, info);
return this.expand(info, len);
}
}
class InitialProtection {
constructor(label, cid) {
var qhkdf = QHKDF.extract(SHA256, INITIAL_SALT, cid);
log('initial_secret', qhkdf.prk);
qhkdf = new QHKDF(qhkdf.hmac, qhkdf.expand_label(label, 32));
log(label + ' secret', qhkdf.prk);
this.key = qhkdf.expand_label("quic key", 16);
log(label + ' key', this.key);
this.iv = qhkdf.expand_label("quic iv", 12);
log(label + ' iv', this.iv);
this.hp = qhkdf.expand_label("quic hp", 16);
log(label + ' hp', this.hp);
}
generateNonce(counter) {
var nonce = Buffer.from(this.iv);
var m = nonce.readUIntBE(nonce.length - 6, 6);
var x = ((m ^ counter) & 0xffffff) +
((((m / 0x1000000) ^ (counter / 0x1000000)) & 0xffffff) * 0x1000000);
nonce.writeUIntBE(x, nonce.length - 6, 6);
return nonce;
}
// Returns the encrypted data with authentication tag appended. The AAD is
// used, but not added to the output.
encipher(pn, aad, data) {
console.log('encipher pn', pn);
log('encipher aad', aad);
log('encipher data', data);
var nonce = this.generateNonce(pn);
var gcm = crypto.createCipheriv(AES_GCM, this.key, nonce);
gcm.setAAD(aad);
var e = gcm.update(data);
gcm.final();
e = Buffer.concat([e, gcm.getAuthTag()]);
log('enciphered', e);
return e;
}
decipher(pn, aad, data) {
console.log('decipher pn', pn);
log('decipher aad', aad);
log('decipher data', data);
var nonce = this.generateNonce(pn);
var gcm = crypto.createDecipheriv(AES_GCM, this.key, nonce);
gcm.setAAD(aad);
gcm.setAuthTag(data.slice(data.length - 16));
var d = gcm.update(data.slice(0, data.length - 16));
gcm.final();
log('deciphered', d);
return d;
}
// Calculates the header protection mask. Returns 16 bytes of output.
hpMask(sample) {
log('hp sample', sample);
// var ctr = crypto.createCipheriv('aes-128-ctr', this.hp, sample);
// var mask = ctr.update(Buffer.alloc(5));
var ecb = crypto.createCipheriv(AES_ECB, this.hp, Buffer.alloc(0));
var mask = ecb.update(sample);
log('hp mask', mask);
return mask;
}
// XOR b into a.
xor(a, b) {
a.forEach((_, i) => {
a[i] ^= b[i];
});
}
// hdr is everything before the length field
// hdr[0] has the packet number length already in place
// pn is the packet number
// data is the payload (i.e., encoded frames)
encrypt(hdr, pn, data) {
var pn_len = 1 + (hdr[0] & 0x3);
if (pn_len + data.length < 4) {
throw new Error('insufficient length of packet number and payload');
}
var aad = Buffer.alloc(hdr.length + 2 + pn_len);
var offset = hdr.copy(aad);
// Add a length that covers the packet number encoding and the auth tag.
offset = aad.writeUIntBE(0x4000 | (pn_len + data.length + 16), offset, 2);
var pn_offset = offset;
var pn_mask = 0xffffffff >> (8 * (4 - pn_len));
offset = aad.writeUIntBE(pn & pn_mask, offset, pn_len)
log('header', aad);
var payload = this.encipher(pn, aad, data);
var mask = this.hpMask(payload.slice(4 - pn_len, 20 - pn_len));
aad[0] ^= mask[0] & (0x1f >> (aad[0] >> 7));
this.xor(aad.slice(pn_offset), mask.slice(1));
log('masked header', aad);
return Buffer.concat([aad, payload]);
}
cidLen(v) {
if (!v) {
return 0;
}
return v + 3;
}
decrypt(data) {
log('decrypt', data);
if (data[0] & 0x40 !== 0x40) {
throw new Error('missing QUIC bit');
}
if (data[0] & 0x80 === 0) {
throw new Error('short header unsupported');
}
var hdr_len = 1 + 4;
hdr_len += 1 + data[hdr_len]; // DCID
hdr_len += 1 + data[hdr_len]; // SCID
if ((data[0] & 0x30) === 0) { // Initial packet: token.
if ((data[hdr_len] & 0xc0) !== 0) {
throw new Error('multi-byte token length unsupported');
}
hdr_len += 1 + data[hdr_len]; // oops: this only handles single octet lengths.
}
// Skip the length.
hdr_len += 1 << (data[hdr_len] >> 6);
// Now we're at the encrypted bit.
var mask = this.hpMask(data.slice(hdr_len + 4, hdr_len + 20));
var octet0 = data[0] ^ (mask[0] & (0x1f >> (data[0] >> 7)));
var pn_len = (octet0 & 3) + 1;
var hdr = Buffer.from(data.slice(0, hdr_len + pn_len));
hdr[0] = octet0;
log('header', hdr);
this.xor(hdr.slice(hdr_len), mask.slice(1));
log('unmasked header', hdr);
var pn = hdr.readUIntBE(hdr_len, pn_len);
// Important: this doesn't recover PN based on expected value.
// The expectation being that Initial packets won't ever need that.
return this.decipher(pn, hdr, data.slice(hdr.length));
}
}
function pad(hdr, body) {
var pn_len = (hdr[0] & 3) + 1;
var size = 1200 - hdr.length - 2 - pn_len - 16; // Assume 2 byte length.
if (size < 0) {
return body;
}
var padded = Buffer.allocUnsafe(size);
console.log('pad amount', size);
body.copy(padded);
padded.fill(0, body.length);
log('padded', padded);
return padded;
}
function test(role, cid, hdr, pn, body) {
cid = Buffer.from(cid, 'hex');
log('connection ID', cid);
hdr = Buffer.from(hdr, 'hex');
log('header', hdr);
console.log('packet number = ' + pn);
body = Buffer.from(body, 'hex');
log('body', hdr);
if (role === 'client' && (hdr[0] & 0x30) === 0) {
body = pad(hdr, body);
}
var endpoint = new InitialProtection(role + ' in', cid);
var packet = endpoint.encrypt(hdr, pn, body);
log('encrypted packet', packet);
var content = endpoint.decrypt(packet);
log('decrypted content', content);
if (content.compare(body) !== 0) {
throw new Error('decrypted result not the same as the original');
}
}
function hex_cid(cid) {
return '0' + (cid.length / 2).toString(16) + cid;
}
function retry(dcid, scid, odcid) {
var pfx = Buffer.from(hex_cid(odcid), 'hex');
var encoded = Buffer.from('ff' + version + hex_cid(dcid) + hex_cid(scid), 'hex');
var token = Buffer.from('token', 'ascii');
var header = Buffer.concat([encoded, token]);
log('retry header', header);
var aad = Buffer.concat([pfx, header]);
log('retry aad', aad);
var key = Buffer.from('4d32ecdb2a2133c841e4043df27d4430', 'hex');
var nonce = Buffer.from('4d1611d05513a552c587d575', 'hex');
var gcm = crypto.createCipheriv(AES_GCM, key, nonce);
gcm.setAAD(aad);
gcm.update('');
gcm.final();
log('retry', Buffer.concat([header, gcm.getAuthTag()]));
}
var cid = '8394c8f03e515708';
var ci_hdr = 'c3' + version + hex_cid(cid) + '0000';
// This is a client Initial. Unfortunately, the ClientHello currently omits
// the transport_parameters extension.
var crypto_frame = '060040c4' +
'010000c003036660261ff947cea49cce6cfad687f457cf1b14531ba14131a0e8' +
'f309a1d0b9c4000006130113031302010000910000000b000900000673657276' +
'6572ff01000100000a00140012001d0017001800190100010101020103010400' +
'230000003300260024001d00204cfdfcd178b784bf328cae793b136f2aedce00' +
'5ff183d7bb1495207236647037002b0003020304000d0020001e040305030603' +
'020308040805080604010501060102010402050206020202002d00020101001c' +
'00024001';
test('client', cid, ci_hdr, 2, crypto_frame);
// This should be a valid server Initial.
var frames = '0d0000000018410a' +
'020000560303eefce7f7b37ba1d163' +
'2e96677825ddf73988cfc79825df566dc5430b9a04' +
'5a1200130100002e00330024001d00209d3c940d89' +
'690b84d08a60993c144eca684d1081287c834d5311' +
'bcf32bb9da1a002b00020304';
var scid = 'f067a5502a4262b5';
var si_hdr = 'c1' + version + '00' + hex_cid(scid) + '00';
test('server', cid, si_hdr, 1, frames);
retry('', scid, cid);