Skip to content

Instantly share code, notes, and snippets.

@jrudolph
Last active February 20, 2020 16:45
Show Gist options
  • Save jrudolph/680eb6ea93dcba6a1f07021949596975 to your computer and use it in GitHub Desktop.
Save jrudolph/680eb6ea93dcba6a1f07021949596975 to your computer and use it in GitHub Desktop.
"Simple" "Http/3" "client"
import java.math.BigInteger
import java.net.DatagramPacket
import java.net.DatagramSocket
import java.net.InetAddress
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.spec.ECPoint
import java.util.Random
import Helpers._
import akka.util.ByteString
import akka.util.ByteStringBuilder
import javax.crypto.Cipher
import javax.crypto.KeyAgreement
import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
import sun.security.ec.ECPublicKeyImpl
import scala.annotation.tailrec
import scala.util.control.NonFatal
object SimpleQuicClientMain extends App {
val random = new SecureRandom
def uint16BE(i: Int): ByteString =
ByteString(i >> 8, i & 0xff)
def uint24BE(i: Int): ByteString =
ByteString(
i >> 16,
i >> 8 & 0xff,
i & 0xff)
val ecdheKeys = KeyPairGenerator.getInstance("EC")
val pair = ecdheKeys.generateKeyPair()
println("Private: " + pair.getPrivate.getEncoded.hexString)
println("Public: " + pair.getPublic.getEncoded.hexString)
val srcConnId = random.nextByteString(8)
println("Src Conn Id: " + srcConnId.toArray.hexString)
var dstConnId = random.nextByteString(8)
println("Dest Conn Id: " + dstConnId.toArray.hexString)
val originalDstConnId = dstConnId
val token = ByteString.empty //"71 75 69 63 68 65 00 00 00 00 00 00 00 00 00 00 FF FF 5D DA 23 13 2C 2C 67 BD F1 87 1C A9".hexByteString
val initialSalt = "c3eef712c72ebb5a11a7d2432bb46365bef9f502".hexByteArray
val initialSecret = hkdf.extract(initialSalt, dstConnId.toArray)
val clientInitialSecret = hkdfExpandLabel(initialSecret, "client in", Array.empty, 32)
val clientKey = hkdfExpandLabel(clientInitialSecret, "quic key", Array.empty, 16)
val clientIv = hkdfExpandLabel(clientInitialSecret, "quic iv", Array.empty, 12)
val clientHp = hkdfExpandLabel(clientInitialSecret, "quic hp", Array.empty, 16)
val serverInitialSecret = hkdfExpandLabel(initialSecret, "server in", Array.empty, 32)
val serverKey = hkdfExpandLabel(serverInitialSecret, "quic key", Array.empty, 16)
val serverIv = hkdfExpandLabel(serverInitialSecret, "quic iv", Array.empty, 12)
val serverHp = hkdfExpandLabel(serverInitialSecret, "quic hp", Array.empty, 16)
val payloadCipher = Cipher.getInstance("AES/GCM/NoPadding")
val hpCipher = Cipher.getInstance("AES/ECB/NoPadding")
var transcript: ByteString = ByteString.empty
def addToTranscript(handshakeMessage: ByteString): Unit = transcript ++= handshakeMessage
def currentTranscriptHash(): Array[Byte] = {
val digest = MessageDigest.getInstance("SHA-256")
digest.digest((transcript).toArray)
}
var handshakeSecret: Array[Byte] = _
var serverHandshakeSecret: Array[Byte] = _
var clientHandshakeSecret: Array[Byte] = _
var masterSecret: Array[Byte] = _
var transcriptUntilCertificateVerify: Array[Byte] = _
var finishedKey: Array[Byte] = _
var clientApplicationSecret: Array[Byte] = _
var serverApplicationSecret: Array[Byte] = _
//val target = InetAddress.getByName("localhost")
//val targetPort = 4443
val target = InetAddress.getByName("quic.rocks")
val targetPort = 4433
val socket = new DatagramSocket(12346)
socket.setSoTimeout(5000)
def send(packet: ByteString): Unit =
socket.send(new DatagramPacket(packet.toArray, packet.size, target, targetPort))
def createCRYPTO(payload: ByteString): ByteString =
"06 00".hexByteString ++
uint16BE(payload.length + 0x4000) ++ // assuming length < 14 bits
payload // works only for first packet because offset is fixed to 0
def handshakeMessage(msgType: Int, payload: ByteString): ByteString =
ByteString(msgType.toByte) ++ uint24BE(payload.length) ++ payload
var initialPacketNumber = 0
def createInitialPacket(payload0: ByteString): ByteString = {
println("Payload: " + payload0.toArray.hexString)
initialPacketNumber += 1
val packetNumber = initialPacketNumber
require(packetNumber < 32)
val packetNumberLength = 1 // as long as fits into a byte
val padding = math.max(0, 1270 - payload0.size)
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte))
val payloadLength = {
val len = packetNumberLength + payload.length + 16 // AEAD signature
uint16BE(len + 0x4000) // assuming length is < 14 bits
}
val plainHeader =
"C0 FF 00 00 17".hexByteString ++
ByteString(dstConnId.size.toByte) ++ dstConnId ++
ByteString(srcConnId.size.toByte) ++ srcConnId ++
ByteString(token.size.toByte) ++ token ++
payloadLength ++
ByteString(packetNumber.toByte)
println("Plain header: " + plainHeader.toArray.hexString)
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */
val nonce = clientIv.clone()
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader.toArray)
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray))
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1
println(s"Sample: ${sample.toArray.hexString}")
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES"))
val mask = hpCipher.doFinal(sample.toArray)
println(s"Mask: ${mask.hexString}")
val protectedHeader = plainHeader.toArray
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x0f)).toByte
val packetNumberOffset = 10 + dstConnId.size + srcConnId.size + token.size
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte
ByteString(protectedHeader ++ encryptedPayload)
}
var handshakePacketNumber = 0
def createHandshakePacket(payload0: ByteString): ByteString = {
println("Payload: " + payload0.toArray.hexString)
handshakePacketNumber += 1
val packetNumber = handshakePacketNumber
require(packetNumber < 200)
val packetNumberLength = 1 // as long as fits into a byte
val padding = math.max(0, 1270 - payload0.size)
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte))
val payloadLength = {
val len = packetNumberLength + payload.length + 16 // AEAD signature
uint16BE(len + 0x4000) // assuming length is < 14 bits
}
val plainHeader =
"E0 FF 00 00 17".hexByteString ++
ByteString(dstConnId.size.toByte) ++ dstConnId ++
ByteString(srcConnId.size.toByte) ++ srcConnId ++
payloadLength ++
ByteString(packetNumber.toByte)
println("Plain header: " + plainHeader.toArray.hexString)
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */
val clientKey = hkdfExpandLabel(clientHandshakeSecret, "quic key", Array.empty, 16)
val clientIv = hkdfExpandLabel(clientHandshakeSecret, "quic iv", Array.empty, 12)
val clientHp = hkdfExpandLabel(clientHandshakeSecret, "quic hp", Array.empty, 16)
val nonce = clientIv.clone()
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader.toArray)
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray))
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1
println(s"Sample: ${sample.toArray.hexString}")
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES"))
val mask = hpCipher.doFinal(sample.toArray)
println(s"Mask: ${mask.hexString}")
val protectedHeader = plainHeader.toArray
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x0f)).toByte
val packetNumberOffset = 9 + dstConnId.size + srcConnId.size
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte
ByteString(protectedHeader ++ encryptedPayload)
}
var shortPacketNumber = 0
def createShortPacket(payload0: ByteString): ByteString = {
println("Payload: " + payload0.toArray.hexString)
shortPacketNumber += 1
val packetNumber = shortPacketNumber
require(packetNumber < 200)
val packetNumberLength = 1 // as long as fits into a byte
val padding = math.max(0, 500 - payload0.size)
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte))
val plainHeader =
"40".hexByteString ++ // keeping spin and key phase to 0, pn_length = 0
dstConnId ++
ByteString(packetNumber.toByte)
println("Plain header: " + plainHeader.toArray.hexString)
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */
val clientKey = hkdfExpandLabel(clientApplicationSecret, "quic key", Array.empty, 16)
val clientIv = hkdfExpandLabel(clientApplicationSecret, "quic iv", Array.empty, 12)
val clientHp = hkdfExpandLabel(clientApplicationSecret, "quic hp", Array.empty, 16)
val nonce = clientIv.clone()
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader.toArray)
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray))
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1
println(s"Sample: ${sample.toArray.hexString}")
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES"))
val mask = hpCipher.doFinal(sample.toArray)
println(s"Mask: ${mask.hexString}")
val protectedHeader = plainHeader.toArray
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x1f)).toByte
val packetNumberOffset = 1 + dstConnId.size
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte
ByteString(protectedHeader ++ encryptedPayload)
}
def nvarlen(i: Long): ByteString = {
require(i >= 0, s"varlen must be positive but was [$i]")
require(i < 63, s"too stupid to encode larger varlens like [$i]")
ByteString(i.toByte)
}
def ack(largestAcknowledged: Long, ackDelay: Long, ackRanges: Seq[Long]): ByteString = {
require(ackRanges.isEmpty)
nvarlen(0x2) ++ //ACK
nvarlen(largestAcknowledged) ++
nvarlen(ackDelay) ++
nvarlen(0) ++ nvarlen(0)
}
def createInitialClientPayload(): ByteString = {
def createClientHello(): ByteString = {
def ext(tpe: Int, payload: ByteString): ByteString =
uint16BE(tpe) ++ uint16BE(payload.size) ++ payload
val keyShareData = {
val ecPublic = pair.getPublic.asInstanceOf[ECPublicKey]
def leftPaddedTo(length: Int, bytes: ByteString): ByteString =
if (bytes.length == length + 1 && bytes(0) == 0) bytes.drop(1)
else {
require(bytes.length <= length)
ByteString(new Array[Byte](length - bytes.length)) ++ bytes
}
val x = ByteString(ecPublic.getW.getAffineX.toByteArray)
val y = ByteString(ecPublic.getW.getAffineY.toByteArray)
println(s"X size: ${x.size} Y size: ${y.size}")
val len = ecPublic.getParams.getCurve.getField.getFieldSize / 8
assert("ec field size", 32, len)
val keyMaterial = "04".hexByteString ++ leftPaddedTo(len, x) ++ leftPaddedTo(len, y)
// only valid for ec params
val res = "00 17".hexByteString ++ uint16BE(keyMaterial.size) ++ keyMaterial
println(res.toArray.hexString)
res
}
val extensions: ByteString = Seq(
ext(0x00, "00 0d 00 00 0a 71 75 69 63 2e 72 6f 63 6b 73".hexByteString), // server name = quic.rocks
ext(0x0a, "00 02 00 17".hexByteString), // supported groups, only secp256r1 (because I found out how to generate keys for that) JDK 11 also knows about which would be preferred x25519
ext(0x0d, "00 02 08 04".hexByteString), // support signature, only rsa_pss_rsae_sha256 (we do not verify signatures, so whatever)
ext(0x10, "00 06 05 68 33 2d 32 33".hexByteString), // alpn: "h3-23"
ext(0x2b, "02 03 04".hexByteString), // supported version tls 1.3
ext(0x33, uint16BE(keyShareData.length) ++ keyShareData), // keyshare for above created pair
"FF A5 00 9B 00 99 00 01 00 04 80 00 75 30 00\n03 00 02 45 C0 00 04 00 04 80 F0 00 00 00 05 00\n04 80 00 40 00 00 06 00 04 80 00 40 00 00 07 00\n04 80 00 40 00 00 08 00 02 40 64 00 09 00 02 40\n67 47 51 00 53 00 00 00 00 04 00 00 00 55 41 49\n44 23 00 00 00 53 43 4C 53 27 00 00 00 43 4F 50\n54 27 00 00 00 49 52 54 54 2B 00 00 00 64 65 76\n20 43 68 72 6F 6D 65 2F 37 39 2E 30 2E 33 39 32\n38 2E 34 20 4C 69 6E 75 78 20 78 38 36 5F 36 34\n01 00 00 00 85 26 02 00 47 52 00 04 FF 00 00 17".hexByteString,
//ext(0xffa5, "00 08 00 01 00 04 80 00 75 30".hexByteString), // empty transport parameters
ext(0x15, ByteString(new Array[Byte](262))) // padding
).reduce(_ ++ _)
"03 03".hexByteString ++ // legacy version
ByteString(random.nextByteString(32)) ++
"20".hexByteString ++ ByteString(random.nextByteString(32)) ++ // legacy session id
"00 02 13 01".hexByteString ++ // just offer TLS_AES_128_GCM_SHA256 (good to be able to use the same hkdf for initial and later encryption levels)
"01 00".hexByteString ++ // legacy compression method "null"
//"00 00".hexByteString ++
uint16BE(extensions.length) ++ extensions
}
createCRYPTO {
val clientHello = handshakeMessage(
0x01,
createClientHello()
)
addToTranscript(clientHello)
clientHello
}
}
// Bytes that couldn't yet be read from a CRYPTO frame because data was still missing
var stashedCryptoBytes: Array[Byte] = Array.empty
var cryptoOffsets: Array[Long] = new Array[Long](4)
def assert[T](what: String, expected: T, actual: T): Unit =
require(expected == actual, s"expected [$what] as [$expected] but got [$actual]")
def analyzeInitialServerPacket(data: Array[Byte]): Unit = {
val reader = new ByteReader(data)
import reader._
@tailrec def handleNextPacket(): Unit =
if (reader.hasRemaining) {
val maskedFlags = byte()
if ((maskedFlags & 0x80) == 0) // short header
handleShortHeaderPacket(maskedFlags)
else {
val packetType = (maskedFlags & 0xf0)
packetType match {
case 0xc0 => handleInitial(maskedFlags)
case 0xe0 => handleHandshake(maskedFlags)
case 0xf0 => handleRetry()
}
}
handleNextPacket()
}
handleNextPacket()
def handleInitial(maskedFlags: Int): Unit = {
//assert("packet type", 0xc0, packetType)
val version = uint32BE()
assert("version", 0xff000017L, version)
val dcidlen = byte()
val dcid = bytes(dcidlen)
println(s"received dest id ${dcid.toArray.hexString}")
val srcLen = byte()
val src = bytes(srcLen)
dstConnId = src
//val src = dstConnId
if (dstConnId != originalDstConnId)
println(s"Server changed its connId to $dstConnId")
val initialSecret = hkdf.extract(initialSalt, originalDstConnId.toArray)
val serverInitialSecret = hkdfExpandLabel(initialSecret, "server in", Array.empty, 32)
val serverKey = hkdfExpandLabel(serverInitialSecret, "quic key", Array.empty, 16)
val serverIv = hkdfExpandLabel(serverInitialSecret, "quic iv", Array.empty, 12)
val serverHp = hkdfExpandLabel(serverInitialSecret, "quic hp", Array.empty, 16)
val tokenLength = varlen()
assert("tokenLength", 0, tokenLength)
val payloadLength = varlen()
// assert("payloadLength", payloadLength, remaining) // otherwise more frames or packets in this packet
val afterLength = pos()
skip(4)
val sample = byteArray(16)
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES"))
val mask = hpCipher.doFinal(sample)
println(s"Mask: ${mask.hexString}")
val unmaskedFlagBits = (maskedFlags & 0x0f) ^ (mask(0) & 0x0f)
println(s"Flags: ${unmaskedFlagBits.toBinaryString}")
assert("reserved", 0, unmaskedFlagBits & 0x0c)
val pnLength = (unmaskedFlagBits & 0x03) + 1
setPos(afterLength)
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long =
if (remaining == 0) res
else {
val next = (byte() ^ mask(atMask)).toByte
readPN(remaining - 1, atMask + 1, res << 8 | next)
}
val packetNumber = readPN(pnLength, 1, 0L)
println(s"PacketNumber: $packetNumber")
//assert("packetNumber", 1, packetNumber)
val plainHeader = data.take(pos())
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x0f)).toByte
def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit =
if (remaining > 0) {
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte
unmaskHeader(remaining - 1, atMask + 1, pos + 1)
}
unmaskHeader(pnLength, 1, afterLength)
println(s"Plain header: ${plainHeader.hexString}")
val encryptedPayload = byteArray(payloadLength.toInt - pnLength) // FIXME, int?
val nonce = serverIv.clone()
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader)
val payload = payloadCipher.doFinal(encryptedPayload)
println(s"Payload: ${payload.hexString}")
send(createInitialPacket(ack(packetNumber, 0, Nil)))
readFrames(payload, 0)
}
def readFrames(payload: Array[Byte], encryptionLevel: Int): Unit = {
val payloadReader = new ByteReader(payload)
@tailrec def readNextFrame(): Unit =
if (payloadReader.hasRemaining) {
val frameType = payloadReader.varlen()
frameType match {
case 0x00 => // padding
case 0x01 => // PING
// nothing to do here
case 0x02 => // ACK
val largestAcknowledged = payloadReader.varlen()
val delay = payloadReader.varlen()
val count = payloadReader.varlen()
val first = payloadReader.varlen()
println(s"Got ACK largest: $largestAcknowledged delay: $delay count: $count first: $first")
case 0x06 => // CRYPTO
val offset = payloadReader.varlen()
val len = payloadReader.varlen()
val cryptoLoad = payloadReader.byteArray(len.toInt)
println(s"Got CRYPTO frame at offset $offset (encLevel: $encryptionLevel) of size $len: ${cryptoLoad.hexString}")
val cryptoOffset = cryptoOffsets(encryptionLevel)
if (offset != cryptoOffset) {
println(s"XXXXX Got retransmission on encLevel $encryptionLevel? offset: $offset != cryptoOffset $cryptoOffset, ignoring frame")
} else {
cryptoOffsets(encryptionLevel) = offset + len
val cryptoLoadReader = new ByteReader(stashedCryptoBytes ++ cryptoLoad)
@tailrec def readNextHandshakeMessage(): Unit = if (cryptoLoadReader.hasRemaining) {
val handshakeMessageTpe = cryptoLoadReader.byte()
println(s"Got handshake message of type [$handshakeMessageTpe]")
val length = cryptoLoadReader.uint24BE()
if (length <= cryptoLoadReader.remaining) {
val handshakeMessage = cryptoLoadReader.bytes(length)
addToTranscript(ByteString(handshakeMessageTpe.toByte) ++ SimpleQuicClientMain.uint24BE(length) ++ handshakeMessage)
handshakeMessageTpe match {
case 2 => // server_hello
println("Got ServerHello")
readServerHelloCRYPTO(handshakeMessage)
case 8 => // encrypted_extensions
println("got encrypted_extensions")
case 11 => // certificate
println("got certificate")
case 15 => // certificate_verify
println("got certificate_verify")
transcriptUntilCertificateVerify = currentTranscriptHash()
case 20 => // finished
println("got finished")
assert("finished size", 32, handshakeMessage.length)
val verifyData = handshakeMessage
println(s"finished verify_data: ${verifyData.toArray.hexString}")
val transcriptHash = currentTranscriptHash()
clientApplicationSecret = hkdfExpandLabel(masterSecret, "c ap traffic", transcriptHash, 32)
println(s"clientApplicationSecret: ${clientApplicationSecret.hexString}")
serverApplicationSecret = hkdfExpandLabel(masterSecret, "s ap traffic", transcriptHash, 32)
println(s"serverApplicationSecret: ${serverApplicationSecret.hexString}")
finishedKey = hkdfExpandLabel(serverHandshakeSecret, "finished", Array.empty, 32)
println(s"finishedKey: ${finishedKey.hexString}")
val mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(finishedKey, "HmacSHA256"))
val check = mac.doFinal(transcriptUntilCertificateVerify)
assert("finish verification", ByteString(check), verifyData)
// FIXME: verify certificate
sendHandshakeFinished()
}
readNextHandshakeMessage()
} else {
println(s"Couldn't read next handshake message because some bytes were still missing. Stashing ${cryptoLoadReader.remaining} bytes (needed $length)")
cryptoLoadReader.setPos(cryptoLoadReader.pos() - 4) // rewind to get behind message type and length
stashedCryptoBytes = cryptoLoadReader.remainingByteArray
}
}
readNextHandshakeMessage()
}
case x if x >= 0x08 && x <= 0x0f => // STREAM
println("Got STREAM")
val offF = (x & 0x4) != 0
val lenF = (x & 0x2) != 0
val fin = (x & 0x1) != 0
println(s"flags off: $offF len: $lenF fin: $fin")
val streamId = payloadReader.varlen()
println(s"streamId: $streamId server initiated: ${(streamId & 0x1) != 0} bidi: ${(streamId & 0x2) == 0}")
val off = if (offF) payloadReader.varlen() else 0L
val len = if (lenF) payloadReader.varlen() else -1
println(s"off: $off len: $len")
val data = if (len == -1) payloadReader.remainingByteArray else payloadReader.byteArray(len.toInt)
println(s"data: ${data.hexString}")
handleStreamData(streamId, off, fin, data)
case 0x10 => // MAX_DATA
val maxData = payloadReader.varlen()
println(s"Got MAX_DATA $maxData")
case x @ (0x12 | 0x13) => // MAX_STREAMS
val bidi = (x & 1) != 0
val maxStreams = payloadReader.varlen()
println(s"Got MAX_STREAMS $maxStreams bidi $bidi")
case 0x1c => // CONNECTION_CLOSE
val errorCode = payloadReader.varlen()
val frameType = payloadReader.varlen()
val reasonPhraseLen = payloadReader.varlen().toInt
val reasonPhrase = payloadReader.string(reasonPhraseLen)
println(s"Got error [$errorCode] '$reasonPhrase'")
case x =>
println(s"Got frame of unknown type ${frameType.toHexString}")
???
}
readNextFrame()
}
readNextFrame()
}
def readServerHelloCRYPTO(payload: ByteString): Unit = {
val reader = new ByteReader(payload.toArray)
import reader._
val version = reader.uint16BE()
assert("tls legacy version", 0x0303, version)
reader.bytes(32)
val sessionLength = reader.byte()
val session = reader.bytes(sessionLength)
val cipherSuite = reader.uint16BE()
assert("cipher suite", 0x1301, cipherSuite) // TLS_AES_128_GCM_SHA256
val compressionMethod = reader.byte()
assert("legacy compression method", 0, compressionMethod)
val extensionLength = reader.uint16BE()
@tailrec def readNextExtension(): Unit =
if (reader.hasRemaining) {
val tpe = reader.uint16BE()
val length = reader.uint16BE()
tpe match {
case 0x2b => // version
assert("version length", 2, length)
val version = reader.uint16BE()
assert("tls version", 0x0304, version)
case 0x33 => // key_share
val group = reader.uint16BE()
val keySize = reader.uint16BE()
val legacyForm = reader.byte()
val key = reader.byteArray(keySize - 1)
assert("DH group", 0x0017, group)
assert("ECDHE legacyForm", 4, legacyForm)
println(s"Got keymaterial of size ${key.size}: ${key.hexString}")
assert("ECDHE key size", 64, key.length)
val x = new BigInteger(key.take(32))
val y = new BigInteger(key.drop(32))
println(s"x: $x y: $y")
val agreement = KeyAgreement.getInstance("ECDH")
agreement.init(pair.getPrivate)
val params = pair.getPrivate.asInstanceOf[ECPrivateKey].getParams
val ecPublic = new ECPublicKeyImpl(new ECPoint(x, y), params) // FIXME: is there a way to do that with public API?
agreement.doPhase(ecPublic, true)
val secret = agreement.generateSecret()
println(s"Calculated secret of size ${secret.size}: ${secret.hexString}")
val digest = MessageDigest.getInstance("SHA-256")
val emptyHash = digest.digest(Array.empty[Byte])
val early = hkdf.extract(new Array[Byte](32), new Array[Byte](32))
println(s"Early: ${early.hexString}")
val preHandshake = hkdfExpandLabel(early, "derived", emptyHash, 32)
println(s"Pre handshake: ${preHandshake.hexString}")
handshakeSecret = hkdf.extract(preHandshake, secret)
println(s"Derived handshakeSecret: ${handshakeSecret.hexString}")
val transcriptHash = currentTranscriptHash()
serverHandshakeSecret = hkdfExpandLabel(handshakeSecret, "s hs traffic", transcriptHash, 32)
println(s"serverHandshakeTrafficSecret: ${serverHandshakeSecret.hexString}")
clientHandshakeSecret = hkdfExpandLabel(handshakeSecret, "c hs traffic", transcriptHash, 32)
println(s"clientHandshakeTrafficSecret: ${clientHandshakeSecret.hexString}")
val preMaster = hkdfExpandLabel(handshakeSecret, "derived", emptyHash, 32)
masterSecret = hkdf.extract(preMaster, new Array[Byte](32))
println(s"masterSecret: ${masterSecret.hexString}")
case x =>
println(s"Unknown extension [0x${x.toHexString}]")
reader.skip(length)
}
readNextExtension()
}
readNextExtension()
}
def handleHandshake(maskedFlags: Int): Unit = {
println("Got Handshake packet")
val startOfHeader = pos() - 1
val quicVersion = uint32BE()
assert("quicVersion", 0xff000017L, quicVersion)
val dcidlen = byte()
val dcid = bytes(dcidlen)
assert("our dcid matches", srcConnId, dcid)
val srcLen = byte()
val src0 = bytes(srcLen)
val src = dstConnId
assert("their dcid matches", dstConnId, src0)
val payloadLength = varlen()
println(s"Payload size: $payloadLength")
val serverKey = hkdfExpandLabel(serverHandshakeSecret, "quic key", Array.empty, 16)
val serverIv = hkdfExpandLabel(serverHandshakeSecret, "quic iv", Array.empty, 12)
val serverHp = hkdfExpandLabel(serverHandshakeSecret, "quic hp", Array.empty, 16)
println(s"Handshake ServerKey: ${serverKey.hexString}")
println(s"Handshake ServerIv: ${serverIv.hexString}")
println(s"Handshake ServerHp: ${serverHp.hexString}")
val afterLength = pos()
skip(4)
val sample = byteArray(16)
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES"))
val mask = hpCipher.doFinal(sample)
println(s"Mask: ${mask.hexString}")
val unmaskedFlagBits = (maskedFlags & 0x0f) ^ (mask(0) & 0x0f)
println(s"Flags: ${unmaskedFlagBits.toBinaryString}")
assert("reserved", 0, unmaskedFlagBits & 0x0c)
val pnLength = (unmaskedFlagBits & 0x03) + 1
setPos(afterLength)
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long =
if (remaining == 0) res
else {
val next = (byte() ^ mask(atMask)).toByte
readPN(remaining - 1, atMask + 1, res << 8 | next)
}
val packetNumber = readPN(pnLength, 1, 0L)
println(s"PacketNumber: $packetNumber")
//assert("packetNumber", 1, packetNumber)
val oldPos = pos()
setPos(startOfHeader)
val bytesToRead = oldPos - startOfHeader
println(s"bytesToRead: ${bytesToRead}")
val plainHeader = byteArray(oldPos - startOfHeader)
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x0f)).toByte
@tailrec def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit =
if (remaining > 0) {
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte
unmaskHeader(remaining - 1, atMask + 1, pos + 1)
}
unmaskHeader(pnLength, 1, afterLength - startOfHeader)
println(s"Plain header: ${plainHeader.hexString}")
val encryptedPayload = byteArray(payloadLength.toInt - pnLength) // FIXME, int?
val nonce = serverIv.clone()
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader)
val payload = payloadCipher.doFinal(encryptedPayload)
println(s"Payload: ${payload.hexString}")
send(createHandshakePacket(ack(packetNumber, 0, Nil)))
readFrames(payload, 1)
}
def handleShortHeaderPacket(maskedFlags: Int): Unit = {
val startOfHeader = pos() - 1
val remaining = remainingByteArray
println(s"Got short header packet ${(maskedFlags.toByte +: remaining).hexString}")
setPos(startOfHeader + 1)
println(s"srcConnId: ${srcConnId.toArray.hexString}")
val dst = bytes(srcConnId.length)
println(s"dst: ${dst.toArray.hexString}")
assert("dcid", srcConnId, dst)
val serverKey = hkdfExpandLabel(serverApplicationSecret, "quic key", Array.empty, 16)
val serverIv = hkdfExpandLabel(serverApplicationSecret, "quic iv", Array.empty, 12)
val serverHp = hkdfExpandLabel(serverApplicationSecret, "quic hp", Array.empty, 16)
println(s"Application ServerKey: ${serverKey.hexString}")
println(s"Application ServerIv: ${serverIv.hexString}")
println(s"Application ServerHp: ${serverHp.hexString}")
val beforePacketNumber = pos()
skip(4)
val sample = byteArray(16)
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES"))
val mask = hpCipher.doFinal(sample)
println(s"Mask: ${mask.hexString}")
val unmaskedFlagBits = (maskedFlags & 0x1f) ^ (mask(0) & 0x1f)
println(s"Flags: ${unmaskedFlagBits.toBinaryString}")
val spin = (unmaskedFlagBits & 0x20) != 0
println(s"Spin: $spin")
assert("reserved", 0, unmaskedFlagBits & 0x18)
val keyPhase = (unmaskedFlagBits & 0x04) != 0
println(s"Keyphase: $keyPhase")
val pnLength = (unmaskedFlagBits & 0x03) + 1
setPos(beforePacketNumber)
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long =
if (remaining == 0) res
else {
val next = (byte() ^ mask(atMask)).toByte
readPN(remaining - 1, atMask + 1, res << 8 | next)
}
val packetNumber = readPN(pnLength, 1, 0L)
println(s"PacketNumber: $packetNumber")
//assert("packetNumber", 1, packetNumber)
val oldPos = pos()
setPos(startOfHeader)
val bytesToRead = oldPos - startOfHeader
println(s"bytesToRead: ${bytesToRead}")
val plainHeader = byteArray(oldPos - startOfHeader)
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x1f)).toByte
@tailrec def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit =
if (remaining > 0) {
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte
unmaskHeader(remaining - 1, atMask + 1, pos + 1)
}
unmaskHeader(pnLength, 1, beforePacketNumber - startOfHeader)
println(s"Plain header: ${plainHeader.hexString}")
val encryptedPayload = remainingByteArray
println(s"Encrypted payload: ${encryptedPayload.hexString}")
val nonce = serverIv.clone()
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce))
payloadCipher.updateAAD(plainHeader)
val payload = payloadCipher.doFinal(encryptedPayload)
println(s"Payload: ${payload.hexString}")
send(createShortPacket(ack(packetNumber, 0, Nil)))
readFrames(payload, 2)
}
def handleRetry(): Unit = {
println("Got Retry")
//assert("packet type", 0xf0, packetType)
val version = uint32BE()
assert("version", 0xff000017L, version)
val dcidlen = byte()
val dcid = bytes(dcidlen)
val srclen = byte()
val src = bytes(srclen)
val odcidlen = byte()
val ocdid = bytes(odcidlen)
assert("original cid", dstConnId, ocdid)
val token = remainingByteArray
println(s"Got token: ${token.hexString}, src: ${src.toArray.hexString}")
}
}
def sendHandshakeFinished(): Unit = {
val finishedKey = hkdfExpandLabel(clientHandshakeSecret, "finished", Array.empty, 32)
println(s"finishedKey: ${finishedKey.hexString}")
val mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(finishedKey, "HmacSHA256"))
val verifyData = mac.doFinal(currentTranscriptHash())
val finishedData = ByteString(verifyData)
val cryptoFrame = createCRYPTO(handshakeMessage(0x14 /* Finished */ , finishedData))
println(s"Sending Finished CRYPTO [${cryptoFrame.toArray.hexString}]")
send(createHandshakePacket(cryptoFrame))
sendSettings()
}
def sendSettings(): Unit = {
val settingsStream = "0A 02 07 00 04 04 01 00 07 00".hexByteString // same as received from server
val authority = s"${target.getHostName}:$targetPort"
val path = "/"
val userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0"
val headerBlock: ByteString =
"00 00 d1 d7 50".hexByteString ++ ByteString(authority.length.toByte) ++ ByteString(authority) ++
"51".hexByteString ++ ByteString(path.length.toByte) ++ ByteString(path) //++
// "5f 50".hexByteString ++ ByteString(userAgent.length.toByte) ++ ByteString(userAgent)
val headersFrame: ByteString = "01".hexByteString ++ nvarlen(headerBlock.size) ++ headerBlock
val request = "0B 08".hexByteString ++ nvarlen(headersFrame.size) ++ headersFrame
send(createShortPacket(settingsStream ++ request))
}
var stashedDataForStream8: Array[Byte] = Array.empty
def handleStreamData(streamId: Long, offset: Long, fin: Boolean, data: Array[Byte]): Unit = streamId match {
case 0x8 =>
println(s"Got response for stream 8 !!! offset $offset fin $fin")
val streamReader = new ByteReader(stashedDataForStream8 ++ data)
stashedDataForStream8 = Array.empty
@tailrec def handleHttp3Frame(): Unit = if (streamReader.hasRemaining) {
val startOfFrame = streamReader.pos()
val tpe = streamReader.varlen()
val len = streamReader.varlen()
if (len > streamReader.remaining) {
println(s"frame not complete need to wait for more data. Needed $len but only got ${streamReader.remaining}")
streamReader.setPos(startOfFrame)
stashedDataForStream8 = streamReader.remainingByteArray
println(s"stashed ${stashedDataForStream8.hexString}")
} else {
val payload = streamReader.bytes(len.toInt)
println(s"Got HTTP3 frame of type $tpe len $len (remaining ${streamReader.remaining})")
try tpe match {
case 0x0 => // DATA
println(s"Got response DATA: [${payload.utf8String}]")
case 0x1 => // HEADERS
println(s"Got response HEADERS ${payload.toArray.hexString}")
val payloadReader = new ByteReader(payload.toArray)
val reqInsertCount = payloadReader.byte() // FIXME 8-bit prefixed
val deltaBaseAndFlag = payloadReader.byte()
val sign = (deltaBaseAndFlag & 0x80) != 0
val deltaBase = deltaBaseAndFlag & 0x7f
println(s"Prefix: reqInsertCount $reqInsertCount sign $sign deltaBase $deltaBase")
QPackTable.readField(payloadReader)
}
catch {
case NonFatal(ex) =>
ex.printStackTrace()
}
handleHttp3Frame()
}
}
handleHttp3Frame()
case x =>
println(s"Got data for stream $streamId at offset $offset ${data.hexString}")
}
val initialPacket = createInitialPacket(createInitialClientPayload())
println(s"Initial packet: ${initialPacket.toArray.hexString}")
send(initialPacket)
val receiveBuffer = new Array[Byte](2000)
val receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.size)
def receiveOne(): Unit = {
socket.receive(receivePacket)
val receivedData = receivePacket.getData.take(receivePacket.getLength)
println(s"Packet received! $receivePacket ${receivedData.hexString}")
analyzeInitialServerPacket(receivedData)
}
while (true) receiveOne()
}
class ByteReader(data: Array[Byte]) {
private[this] var offset = 0
def remaining: Int = data.length - offset
def hasRemaining: Boolean = offset < data.length
def byte(): Int = {
val res = data(offset) & 0xff
offset += 1
res
}
def uint16BE(): Int = byte() << 8 | byte()
def uint24BE(): Int = (uint16BE() << 8) | byte()
def uint32BE(): Long = (uint24BE().toLong << 8L) | byte().toLong
def byteArray(len: Int): Array[Byte] = {
require(len <= remaining, s"Wanted to read $len further bytes but only have $remaining")
val res = data.slice(offset, offset + len)
offset += len
res
}
def bytes(len: Int): ByteString = ByteString(byteArray(len))
def remainingByteArray: Array[Byte] = {
val res = data.drop(offset)
offset = data.length
res
}
def varlen(): Long = {
val first = byte()
val flags = (first & 0xc0) >> 6
val firstData = first & 0x3f
flags match {
case 0 => first
case 1 => firstData << 8 | byte()
case 2 => firstData << 24 | uint24BE()
case 3 => firstData << 56 | uint24BE() << 32 | uint24BE() << 8 | byte()
}
}
def string(len: Int): String = bytes(len).utf8String
def skip(len: Int): Unit = offset += len
def pos(): Int = offset
def setPos(newPos: Int): Unit = offset = newPos
def prefixed(prefixLength: Int, prefix: Int): Long =
if (Integer.numberOfTrailingZeros(~prefix) == prefixLength) {
@tailrec def readOne(curV: Long, shift: Int): Long = {
val b = byte()
val newBits = b & 0x7f
val value = curV | (newBits << shift)
if ((b & 0x80) == 0) value
else readOne(value, shift + 7)
}
readOne(0, 0) + prefix
} else prefix
}
object QPackTable {
@tailrec def readField(payloadReader: ByteReader): Unit = if (payloadReader.hasRemaining) {
val b = payloadReader.byte()
if ((b & 0x80) != 0) { // indexed header field
val static = (b & 0x40) != 0
val indexP = b & 0x3f
val index = payloadReader.prefixed(6, indexP)
println(s"Indexed Header Field static $static idx $index")
println(s"Decoded: ${QPackTable.entries(index.toInt)}")
} else if ((b & 0x40) != 0) { // literal header field with name reference
val nbit = (b & 0x20) != 0
val nameS = (b & 0x10) != 0
val nameIdxP = b & 0x0f
val nameIdx = payloadReader.prefixed(4, nameIdxP)
val valueB = payloadReader.byte()
val huffman = (valueB & 0x80) != 0
val valueLP = valueB & 0x7f
val valueL = payloadReader.prefixed(7, valueLP)
val value = payloadReader.bytes(valueL.toInt)
println(s"literal header field with name reference nbit $nbit nameS $nameS nameIdx $nameIdx huffman $huffman valueL $valueL value ${value.toArray.hexString}")
println(s"Decoded: ${QPackTable.entries(nameIdx.toInt)._1} ${decode(huffman, value).utf8String}")
} else if ((b & 0x20) != 0) { // literal header field without name reference
val nbit = (b & 0x10) != 0
val nameH = (b & 0x08) != 0
val nameLP = b & 0x07
val nameL = payloadReader.prefixed(3, nameLP)
val name = payloadReader.bytes(nameL.toInt)
val valueB = payloadReader.byte()
val valueH = (valueB & 0x80) != 0
val valueLP = (valueB & 0x7f)
val valueL = payloadReader.prefixed(7, valueLP)
val value = payloadReader.bytes(valueL.toInt)
println(s"literal header field without name reference nbit $nbit nameH $nameH nameL $nameL name ${name.toArray.hexString} valueH $valueH valueL $valueL value ${value.toArray.hexString}")
println(s"Decoded: ${decode(nameH, name).utf8String} ${decode(valueH, value).utf8String}")
} else if ((b & 0x10) != 0) { // indexed Header Field With Post-Base Index
???
} else { // Literal Header Field With Post-Base Name Reference
???
}
readField(payloadReader)
}
def decode(huffman: Boolean, bytes: ByteString): ByteString =
if (huffman) HPackHuffman.decode(bytes)
else bytes
val entries: Seq[(String, Option[String])] = {
table.split("\n").filter(x => x.length > 43 && x(10) == '|' && (!x(12).isWhitespace || !x(43).isWhitespace))
.drop(1)
.sliding(2)
.flatMap {
case Array(l1, l2) =>
if (l1(5).isDigit && l2(5).isDigit) {
val Array(idx, name, value) = l1.split('|').drop(1).map(_.trim)
(idx.toInt, name, Some(value).filter(_.nonEmpty)) :: Nil
} else if (l1(5).isDigit) {
val Array(idx, name1, value1) = l1.split('|').drop(1).map(_.trim)
val Array(name2, value2) = l2.split('|').drop(2).map(_.trim)
(idx.toInt, name1 + name2, Some(value1 + value2).filter(_.nonEmpty)) :: Nil
} else Nil // ignore
}
.zipWithIndex
.map {
case ((idx, name, value), i) =>
require(idx == i, s"Idx $idx and i $i didn't match")
(name, value)
}
.toSeq
}
private lazy val table =
"""Appendix A. Static Table
|
| +------+-----------------------------+------------------------------+
| | Inde | Name | Value |
| | x | | |
| +------+-----------------------------+------------------------------+
| | 0 | :authority | |
| | | | |
| | 1 | :path | / |
| | | | |
| | 2 | age | 0 |
| | | | |
| | 3 | content-disposition | |
| | | | |
| | 4 | content-length | 0 |
| | | | |
| | 5 | cookie | |
| | | | |
| | 6 | date | |
| | | | |
| | 7 | etag | |
| | | | |
| | 8 | if-modified-since | |
| | | | |
| | 9 | if-none-match | |
| | | | |
| | 10 | last-modified | |
| | | | |
| | 11 | link | |
|
|
|
|Krasic, et al. Expires March 15, 2020 [Page 28]
|
|
|Internet-Draft QPACK September 2019
|
|
| | | | |
| | 12 | location | |
| | | | |
| | 13 | referer | |
| | | | |
| | 14 | set-cookie | |
| | | | |
| | 15 | :method | CONNECT |
| | | | |
| | 16 | :method | DELETE |
| | | | |
| | 17 | :method | GET |
| | | | |
| | 18 | :method | HEAD |
| | | | |
| | 19 | :method | OPTIONS |
| | | | |
| | 20 | :method | POST |
| | | | |
| | 21 | :method | PUT |
| | | | |
| | 22 | :scheme | http |
| | | | |
| | 23 | :scheme | https |
| | | | |
| | 24 | :status | 103 |
| | | | |
| | 25 | :status | 200 |
| | | | |
| | 26 | :status | 304 |
| | | | |
| | 27 | :status | 404 |
| | | | |
| | 28 | :status | 503 |
| | | | |
| | 29 | accept | */* |
| | | | |
| | 30 | accept | application/dns-message |
| | | | |
| | 31 | accept-encoding | gzip, deflate, br |
| | | | |
| | 32 | accept-ranges | bytes |
| | | | |
| | 33 | access-control-allow- | cache-control |
| | | headers | |
| | | | |
| | 34 | access-control-allow- | content-type |
| | | headers | |
|
|
|
|Krasic, et al. Expires March 15, 2020 [Page 29]
|
|
|Internet-Draft QPACK September 2019
|
|
| | | | |
| | 35 | access-control-allow-origin | * |
| | | | |
| | 36 | cache-control | max-age=0 |
| | | | |
| | 37 | cache-control | max-age=2592000 |
| | | | |
| | 38 | cache-control | max-age=604800 |
| | | | |
| | 39 | cache-control | no-cache |
| | | | |
| | 40 | cache-control | no-store |
| | | | |
| | 41 | cache-control | public, max-age=31536000 |
| | | | |
| | 42 | content-encoding | br |
| | | | |
| | 43 | content-encoding | gzip |
| | | | |
| | 44 | content-type | application/dns-message |
| | | | |
| | 45 | content-type | application/javascript |
| | | | |
| | 46 | content-type | application/json |
| | | | |
| | 47 | content-type | application/x-www-form- |
| | | | urlencoded |
| | | | |
| | 48 | content-type | image/gif |
| | | | |
| | 49 | content-type | image/jpeg |
| | | | |
| | 50 | content-type | image/png |
| | | | |
| | 51 | content-type | text/css |
| | | | |
| | 52 | content-type | text/html; charset=utf-8 |
| | | | |
| | 53 | content-type | text/plain |
| | | | |
| | 54 | content-type | text/plain;charset=utf-8 |
| | | | |
| | 55 | range | bytes=0- |
| | | | |
| | 56 | strict-transport-security | max-age=31536000 |
| | | | |
| | 57 | strict-transport-security | max-age=31536000; |
| | | | includesubdomains |
|
|
|
|Krasic, et al. Expires March 15, 2020 [Page 30]
|
|
|Internet-Draft QPACK September 2019
|
|
| | | | |
| | 58 | strict-transport-security | max-age=31536000; |
| | | | includesubdomains; preload |
| | | | |
| | 59 | vary | accept-encoding |
| | | | |
| | 60 | vary | origin |
| | | | |
| | 61 | x-content-type-options | nosniff |
| | | | |
| | 62 | x-xss-protection | 1; mode=block |
| | | | |
| | 63 | :status | 100 |
| | | | |
| | 64 | :status | 204 |
| | | | |
| | 65 | :status | 206 |
| | | | |
| | 66 | :status | 302 |
| | | | |
| | 67 | :status | 400 |
| | | | |
| | 68 | :status | 403 |
| | | | |
| | 69 | :status | 421 |
| | | | |
| | 70 | :status | 425 |
| | | | |
| | 71 | :status | 500 |
| | | | |
| | 72 | accept-language | |
| | | | |
| | 73 | access-control-allow- | FALSE |
| | | credentials | |
| | | | |
| | 74 | access-control-allow- | TRUE |
| | | credentials | |
| | | | |
| | 75 | access-control-allow- | * |
| | | headers | |
| | | | |
| | 76 | access-control-allow- | get |
| | | methods | |
| | | | |
| | 77 | access-control-allow- | get, post, options |
| | | methods | |
| | | | |
| | 78 | access-control-allow- | options |
|
|
|
|Krasic, et al. Expires March 15, 2020 [Page 31]
|
|
|Internet-Draft QPACK September 2019
|
|
| | | methods | |
| | | | |
| | 79 | access-control-expose- | content-length |
| | | headers | |
| | | | |
| | 80 | access-control-request- | content-type |
| | | headers | |
| | | | |
| | 81 | access-control-request- | get |
| | | method | |
| | | | |
| | 82 | access-control-request- | post |
| | | method | |
| | | | |
| | 83 | alt-svc | clear |
| | | | |
| | 84 | authorization | |
| | | | |
| | 85 | content-security-policy | script-src 'none'; object- |
| | | | src 'none'; base-uri 'none' |
| | | | |
| | 86 | early-data | 1 |
| | | | |
| | 87 | expect-ct | |
| | | | |
| | 88 | forwarded | |
| | | | |
| | 89 | if-range | |
| | | | |
| | 90 | origin | |
| | | | |
| | 91 | purpose | prefetch |
| | | | |
| | 92 | server | |
| | | | |
| | 93 | timing-allow-origin | * |
| | | | |
| | 94 | upgrade-insecure-requests | 1 |
| | | | |
| | 95 | user-agent | |
| | | | |
| | 96 | x-forwarded-for | |
| | | | |
| | 97 | x-frame-options | deny |
| | | | |
| | 98 | x-frame-options | sameorigin |
| +------+-----------------------------+------------------------------+""".stripMargin
}
object HPackHuffman {
def decode(bytes: ByteString): ByteString = {
val bb = new ByteStringBuilder
@tailrec def decodeNext(remaining: String): Unit = if (remaining.nonEmpty) {
val i = codes.indexWhere(remaining.startsWith)
i match {
case -1 =>
// done
require(remaining.size < 8, s"Couldn't find prefix when size was ${remaining.size}: [$remaining]")
case 256 => // done
case x =>
bb += x.toByte
decodeNext(remaining.drop(codes(i).length))
}
}
// string-based huffman decoding is really the best
val allBits = bytes.map(b => ((b & 0xff) | 0x100).toBinaryString.takeRight(8)).mkString
decodeNext(allBits)
bb.result()
}
val codes: Seq[String] =
table.split("\n")
.filter(x => x.length > 14 && x(14) == '|')
.map(_.slice(14, 49).filter(_ != '|').trim)
private lazy val table =
""" code
| code as bits as hex len
| sym aligned to MSB aligned in
| to LSB bits
| ( 0) |11111111|11000 1ff8 [13]
| ( 1) |11111111|11111111|1011000 7fffd8 [23]
| ( 2) |11111111|11111111|11111110|0010 fffffe2 [28]
| ( 3) |11111111|11111111|11111110|0011 fffffe3 [28]
| ( 4) |11111111|11111111|11111110|0100 fffffe4 [28]
| ( 5) |11111111|11111111|11111110|0101 fffffe5 [28]
| ( 6) |11111111|11111111|11111110|0110 fffffe6 [28]
| ( 7) |11111111|11111111|11111110|0111 fffffe7 [28]
| ( 8) |11111111|11111111|11111110|1000 fffffe8 [28]
| ( 9) |11111111|11111111|11101010 ffffea [24]
| ( 10) |11111111|11111111|11111111|111100 3ffffffc [30]
| ( 11) |11111111|11111111|11111110|1001 fffffe9 [28]
| ( 12) |11111111|11111111|11111110|1010 fffffea [28]
| ( 13) |11111111|11111111|11111111|111101 3ffffffd [30]
|
|
|
|Peon & Ruellan Standards Track [Page 27]
|
|
|RFC 7541 HPACK May 2015
|
|
| ( 14) |11111111|11111111|11111110|1011 fffffeb [28]
| ( 15) |11111111|11111111|11111110|1100 fffffec [28]
| ( 16) |11111111|11111111|11111110|1101 fffffed [28]
| ( 17) |11111111|11111111|11111110|1110 fffffee [28]
| ( 18) |11111111|11111111|11111110|1111 fffffef [28]
| ( 19) |11111111|11111111|11111111|0000 ffffff0 [28]
| ( 20) |11111111|11111111|11111111|0001 ffffff1 [28]
| ( 21) |11111111|11111111|11111111|0010 ffffff2 [28]
| ( 22) |11111111|11111111|11111111|111110 3ffffffe [30]
| ( 23) |11111111|11111111|11111111|0011 ffffff3 [28]
| ( 24) |11111111|11111111|11111111|0100 ffffff4 [28]
| ( 25) |11111111|11111111|11111111|0101 ffffff5 [28]
| ( 26) |11111111|11111111|11111111|0110 ffffff6 [28]
| ( 27) |11111111|11111111|11111111|0111 ffffff7 [28]
| ( 28) |11111111|11111111|11111111|1000 ffffff8 [28]
| ( 29) |11111111|11111111|11111111|1001 ffffff9 [28]
| ( 30) |11111111|11111111|11111111|1010 ffffffa [28]
| ( 31) |11111111|11111111|11111111|1011 ffffffb [28]
| ' ' ( 32) |010100 14 [ 6]
| '!' ( 33) |11111110|00 3f8 [10]
| '"' ( 34) |11111110|01 3f9 [10]
| '#' ( 35) |11111111|1010 ffa [12]
| '$' ( 36) |11111111|11001 1ff9 [13]
| '%' ( 37) |010101 15 [ 6]
| '&' ( 38) |11111000 f8 [ 8]
| ''' ( 39) |11111111|010 7fa [11]
| '(' ( 40) |11111110|10 3fa [10]
| ')' ( 41) |11111110|11 3fb [10]
| '*' ( 42) |11111001 f9 [ 8]
| '+' ( 43) |11111111|011 7fb [11]
| ',' ( 44) |11111010 fa [ 8]
| '-' ( 45) |010110 16 [ 6]
| '.' ( 46) |010111 17 [ 6]
| '/' ( 47) |011000 18 [ 6]
| '0' ( 48) |00000 0 [ 5]
| '1' ( 49) |00001 1 [ 5]
| '2' ( 50) |00010 2 [ 5]
| '3' ( 51) |011001 19 [ 6]
| '4' ( 52) |011010 1a [ 6]
| '5' ( 53) |011011 1b [ 6]
| '6' ( 54) |011100 1c [ 6]
| '7' ( 55) |011101 1d [ 6]
| '8' ( 56) |011110 1e [ 6]
| '9' ( 57) |011111 1f [ 6]
| ':' ( 58) |1011100 5c [ 7]
| ';' ( 59) |11111011 fb [ 8]
| '<' ( 60) |11111111|1111100 7ffc [15]
| '=' ( 61) |100000 20 [ 6]
|
|
|
|Peon & Ruellan Standards Track [Page 28]
|
|
|RFC 7541 HPACK May 2015
|
|
| '>' ( 62) |11111111|1011 ffb [12]
| '?' ( 63) |11111111|00 3fc [10]
| '@' ( 64) |11111111|11010 1ffa [13]
| 'A' ( 65) |100001 21 [ 6]
| 'B' ( 66) |1011101 5d [ 7]
| 'C' ( 67) |1011110 5e [ 7]
| 'D' ( 68) |1011111 5f [ 7]
| 'E' ( 69) |1100000 60 [ 7]
| 'F' ( 70) |1100001 61 [ 7]
| 'G' ( 71) |1100010 62 [ 7]
| 'H' ( 72) |1100011 63 [ 7]
| 'I' ( 73) |1100100 64 [ 7]
| 'J' ( 74) |1100101 65 [ 7]
| 'K' ( 75) |1100110 66 [ 7]
| 'L' ( 76) |1100111 67 [ 7]
| 'M' ( 77) |1101000 68 [ 7]
| 'N' ( 78) |1101001 69 [ 7]
| 'O' ( 79) |1101010 6a [ 7]
| 'P' ( 80) |1101011 6b [ 7]
| 'Q' ( 81) |1101100 6c [ 7]
| 'R' ( 82) |1101101 6d [ 7]
| 'S' ( 83) |1101110 6e [ 7]
| 'T' ( 84) |1101111 6f [ 7]
| 'U' ( 85) |1110000 70 [ 7]
| 'V' ( 86) |1110001 71 [ 7]
| 'W' ( 87) |1110010 72 [ 7]
| 'X' ( 88) |11111100 fc [ 8]
| 'Y' ( 89) |1110011 73 [ 7]
| 'Z' ( 90) |11111101 fd [ 8]
| '[' ( 91) |11111111|11011 1ffb [13]
| '\' ( 92) |11111111|11111110|000 7fff0 [19]
| ']' ( 93) |11111111|11100 1ffc [13]
| '^' ( 94) |11111111|111100 3ffc [14]
| '_' ( 95) |100010 22 [ 6]
| '`' ( 96) |11111111|1111101 7ffd [15]
| 'a' ( 97) |00011 3 [ 5]
| 'b' ( 98) |100011 23 [ 6]
| 'c' ( 99) |00100 4 [ 5]
| 'd' (100) |100100 24 [ 6]
| 'e' (101) |00101 5 [ 5]
| 'f' (102) |100101 25 [ 6]
| 'g' (103) |100110 26 [ 6]
| 'h' (104) |100111 27 [ 6]
| 'i' (105) |00110 6 [ 5]
| 'j' (106) |1110100 74 [ 7]
| 'k' (107) |1110101 75 [ 7]
| 'l' (108) |101000 28 [ 6]
| 'm' (109) |101001 29 [ 6]
|
|
|
|Peon & Ruellan Standards Track [Page 29]
|
|
|RFC 7541 HPACK May 2015
|
|
| 'n' (110) |101010 2a [ 6]
| 'o' (111) |00111 7 [ 5]
| 'p' (112) |101011 2b [ 6]
| 'q' (113) |1110110 76 [ 7]
| 'r' (114) |101100 2c [ 6]
| 's' (115) |01000 8 [ 5]
| 't' (116) |01001 9 [ 5]
| 'u' (117) |101101 2d [ 6]
| 'v' (118) |1110111 77 [ 7]
| 'w' (119) |1111000 78 [ 7]
| 'x' (120) |1111001 79 [ 7]
| 'y' (121) |1111010 7a [ 7]
| 'z' (122) |1111011 7b [ 7]
| '{' (123) |11111111|1111110 7ffe [15]
| '|' (124) |11111111|100 7fc [11]
| '}' (125) |11111111|111101 3ffd [14]
| '~' (126) |11111111|11101 1ffd [13]
| (127) |11111111|11111111|11111111|1100 ffffffc [28]
| (128) |11111111|11111110|0110 fffe6 [20]
| (129) |11111111|11111111|010010 3fffd2 [22]
| (130) |11111111|11111110|0111 fffe7 [20]
| (131) |11111111|11111110|1000 fffe8 [20]
| (132) |11111111|11111111|010011 3fffd3 [22]
| (133) |11111111|11111111|010100 3fffd4 [22]
| (134) |11111111|11111111|010101 3fffd5 [22]
| (135) |11111111|11111111|1011001 7fffd9 [23]
| (136) |11111111|11111111|010110 3fffd6 [22]
| (137) |11111111|11111111|1011010 7fffda [23]
| (138) |11111111|11111111|1011011 7fffdb [23]
| (139) |11111111|11111111|1011100 7fffdc [23]
| (140) |11111111|11111111|1011101 7fffdd [23]
| (141) |11111111|11111111|1011110 7fffde [23]
| (142) |11111111|11111111|11101011 ffffeb [24]
| (143) |11111111|11111111|1011111 7fffdf [23]
| (144) |11111111|11111111|11101100 ffffec [24]
| (145) |11111111|11111111|11101101 ffffed [24]
| (146) |11111111|11111111|010111 3fffd7 [22]
| (147) |11111111|11111111|1100000 7fffe0 [23]
| (148) |11111111|11111111|11101110 ffffee [24]
| (149) |11111111|11111111|1100001 7fffe1 [23]
| (150) |11111111|11111111|1100010 7fffe2 [23]
| (151) |11111111|11111111|1100011 7fffe3 [23]
| (152) |11111111|11111111|1100100 7fffe4 [23]
| (153) |11111111|11111110|11100 1fffdc [21]
| (154) |11111111|11111111|011000 3fffd8 [22]
| (155) |11111111|11111111|1100101 7fffe5 [23]
| (156) |11111111|11111111|011001 3fffd9 [22]
| (157) |11111111|11111111|1100110 7fffe6 [23]
|
|
|
|Peon & Ruellan Standards Track [Page 30]
|
|
|RFC 7541 HPACK May 2015
|
|
| (158) |11111111|11111111|1100111 7fffe7 [23]
| (159) |11111111|11111111|11101111 ffffef [24]
| (160) |11111111|11111111|011010 3fffda [22]
| (161) |11111111|11111110|11101 1fffdd [21]
| (162) |11111111|11111110|1001 fffe9 [20]
| (163) |11111111|11111111|011011 3fffdb [22]
| (164) |11111111|11111111|011100 3fffdc [22]
| (165) |11111111|11111111|1101000 7fffe8 [23]
| (166) |11111111|11111111|1101001 7fffe9 [23]
| (167) |11111111|11111110|11110 1fffde [21]
| (168) |11111111|11111111|1101010 7fffea [23]
| (169) |11111111|11111111|011101 3fffdd [22]
| (170) |11111111|11111111|011110 3fffde [22]
| (171) |11111111|11111111|11110000 fffff0 [24]
| (172) |11111111|11111110|11111 1fffdf [21]
| (173) |11111111|11111111|011111 3fffdf [22]
| (174) |11111111|11111111|1101011 7fffeb [23]
| (175) |11111111|11111111|1101100 7fffec [23]
| (176) |11111111|11111111|00000 1fffe0 [21]
| (177) |11111111|11111111|00001 1fffe1 [21]
| (178) |11111111|11111111|100000 3fffe0 [22]
| (179) |11111111|11111111|00010 1fffe2 [21]
| (180) |11111111|11111111|1101101 7fffed [23]
| (181) |11111111|11111111|100001 3fffe1 [22]
| (182) |11111111|11111111|1101110 7fffee [23]
| (183) |11111111|11111111|1101111 7fffef [23]
| (184) |11111111|11111110|1010 fffea [20]
| (185) |11111111|11111111|100010 3fffe2 [22]
| (186) |11111111|11111111|100011 3fffe3 [22]
| (187) |11111111|11111111|100100 3fffe4 [22]
| (188) |11111111|11111111|1110000 7ffff0 [23]
| (189) |11111111|11111111|100101 3fffe5 [22]
| (190) |11111111|11111111|100110 3fffe6 [22]
| (191) |11111111|11111111|1110001 7ffff1 [23]
| (192) |11111111|11111111|11111000|00 3ffffe0 [26]
| (193) |11111111|11111111|11111000|01 3ffffe1 [26]
| (194) |11111111|11111110|1011 fffeb [20]
| (195) |11111111|11111110|001 7fff1 [19]
| (196) |11111111|11111111|100111 3fffe7 [22]
| (197) |11111111|11111111|1110010 7ffff2 [23]
| (198) |11111111|11111111|101000 3fffe8 [22]
| (199) |11111111|11111111|11110110|0 1ffffec [25]
| (200) |11111111|11111111|11111000|10 3ffffe2 [26]
| (201) |11111111|11111111|11111000|11 3ffffe3 [26]
| (202) |11111111|11111111|11111001|00 3ffffe4 [26]
| (203) |11111111|11111111|11111011|110 7ffffde [27]
| (204) |11111111|11111111|11111011|111 7ffffdf [27]
| (205) |11111111|11111111|11111001|01 3ffffe5 [26]
|
|
|
|Peon & Ruellan Standards Track [Page 31]
|
|
|RFC 7541 HPACK May 2015
|
|
| (206) |11111111|11111111|11110001 fffff1 [24]
| (207) |11111111|11111111|11110110|1 1ffffed [25]
| (208) |11111111|11111110|010 7fff2 [19]
| (209) |11111111|11111111|00011 1fffe3 [21]
| (210) |11111111|11111111|11111001|10 3ffffe6 [26]
| (211) |11111111|11111111|11111100|000 7ffffe0 [27]
| (212) |11111111|11111111|11111100|001 7ffffe1 [27]
| (213) |11111111|11111111|11111001|11 3ffffe7 [26]
| (214) |11111111|11111111|11111100|010 7ffffe2 [27]
| (215) |11111111|11111111|11110010 fffff2 [24]
| (216) |11111111|11111111|00100 1fffe4 [21]
| (217) |11111111|11111111|00101 1fffe5 [21]
| (218) |11111111|11111111|11111010|00 3ffffe8 [26]
| (219) |11111111|11111111|11111010|01 3ffffe9 [26]
| (220) |11111111|11111111|11111111|1101 ffffffd [28]
| (221) |11111111|11111111|11111100|011 7ffffe3 [27]
| (222) |11111111|11111111|11111100|100 7ffffe4 [27]
| (223) |11111111|11111111|11111100|101 7ffffe5 [27]
| (224) |11111111|11111110|1100 fffec [20]
| (225) |11111111|11111111|11110011 fffff3 [24]
| (226) |11111111|11111110|1101 fffed [20]
| (227) |11111111|11111111|00110 1fffe6 [21]
| (228) |11111111|11111111|101001 3fffe9 [22]
| (229) |11111111|11111111|00111 1fffe7 [21]
| (230) |11111111|11111111|01000 1fffe8 [21]
| (231) |11111111|11111111|1110011 7ffff3 [23]
| (232) |11111111|11111111|101010 3fffea [22]
| (233) |11111111|11111111|101011 3fffeb [22]
| (234) |11111111|11111111|11110111|0 1ffffee [25]
| (235) |11111111|11111111|11110111|1 1ffffef [25]
| (236) |11111111|11111111|11110100 fffff4 [24]
| (237) |11111111|11111111|11110101 fffff5 [24]
| (238) |11111111|11111111|11111010|10 3ffffea [26]
| (239) |11111111|11111111|1110100 7ffff4 [23]
| (240) |11111111|11111111|11111010|11 3ffffeb [26]
| (241) |11111111|11111111|11111100|110 7ffffe6 [27]
| (242) |11111111|11111111|11111011|00 3ffffec [26]
| (243) |11111111|11111111|11111011|01 3ffffed [26]
| (244) |11111111|11111111|11111100|111 7ffffe7 [27]
| (245) |11111111|11111111|11111101|000 7ffffe8 [27]
| (246) |11111111|11111111|11111101|001 7ffffe9 [27]
| (247) |11111111|11111111|11111101|010 7ffffea [27]
| (248) |11111111|11111111|11111101|011 7ffffeb [27]
| (249) |11111111|11111111|11111111|1110 ffffffe [28]
| (250) |11111111|11111111|11111101|100 7ffffec [27]
| (251) |11111111|11111111|11111101|101 7ffffed [27]
| (252) |11111111|11111111|11111101|110 7ffffee [27]
| (253) |11111111|11111111|11111101|111 7ffffef [27]
|
|
|
|Peon & Ruellan Standards Track [Page 32]
|
|
|RFC 7541 HPACK May 2015
|
|
| (254) |11111111|11111111|11111110|000 7fffff0 [27]
| (255) |11111111|11111111|11111011|10 3ffffee [26]
| EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]""".stripMargin
}
object Helpers {
implicit class RichString(val s: String) extends AnyVal {
def hexByteArray: Array[Byte] =
s.filter(_.isLetterOrDigit)
.grouped(2)
.map(x => java.lang.Integer.parseInt(x.mkString, 16).toByte)
.toArray
def hexByteString: ByteString = ByteString(hexByteArray)
}
implicit class RichByteArray(val ba: Array[Byte]) extends AnyVal {
def hexString: String = "[" + ba.map(_ formatted "%02X").mkString(" ") + "]"
}
implicit class RichRandom(val random: Random) extends AnyVal {
def nextByteString(size: Int): ByteString = {
val res = new Array[Byte](size)
random.nextBytes(res)
ByteString(res)
}
}
val hkdf = HKDF("HmacSHA256")
// https://tools.ietf.org/html/rfc8446#section-7.1
def hkdfExpandLabel(secret: Array[Byte], label: String, context: Array[Byte], length: Int): Array[Byte] = {
val lengthBytes = ByteString((length >> 8).toByte, (length & 0xff).toByte)
def lengthPrefixed(bytes: ByteString): ByteString = {
require(bytes.length < 256)
ByteString(bytes.length.toByte) ++ bytes
}
val hkdfLabel = lengthBytes ++ lengthPrefixed(ByteString(s"tls13 $label", "ASCII")) ++ lengthPrefixed(ByteString(context))
hkdf.expand(secret, hkdfLabel.toArray, length)
}
}
class HKDF(mac: Mac) {
def extract(salt: Array[Byte], inputKeyingMaterial: Array[Byte]): Array[Byte] =
hash(
if (salt.isEmpty) new Array[Byte](mac.getMacLength) else salt,
inputKeyingMaterial
)
private def hash(key: Array[Byte], data: Array[Byte]): Array[Byte] = {
mac.init(new SecretKeySpec(key, mac.getAlgorithm))
mac.doFinal(data)
}
def expand(pseudoRandomKey: Array[Byte], info: Array[Byte], length: Int): Array[Byte] = {
require(length <= 255 * mac.getMacLength)
val okm = new Array[Byte](length)
val counter = new Array[Byte](1)
def step(cur: Array[Byte], round: Int): Unit = {
counter(0) = round.toByte
val tNext = hash(pseudoRandomKey, cur ++ info ++ counter) // FIXME: avoid copying
val offset = (round - 1) * mac.getMacLength
val remaining = length - offset
val toCopy = math.min(remaining, tNext.length)
println(s"length: $length round: $round offset: $offset rem: $remaining toCopy $toCopy")
System.arraycopy(tNext, 0, okm, offset, toCopy)
if (offset + mac.getMacLength < length) step(tNext, round + 1)
}
step(Array.emptyByteArray, 1)
okm
}
}
object HKDF {
def apply(algorithmName: String): HKDF = new HKDF(Mac.getInstance(algorithmName))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment