From 6da006190dd1b141596cf531dd309119a13d87ed Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 1 Oct 2026 13:35:13 -0400 Subject: [PATCH 1/2] Encode HKDF labels in temporary storage `Protector` derives every packet protection key, IV and header protection key with an HKDF expansion whose label it encodes first. The encoding went into a fresh array each time, though it only has to last for the one expansion, and a connection derives around two dozen of them across its initial, handshake and application keys. `withEncodedLabel` builds it in temporary storage instead. Measured on my Mac against `main` with the package's QUIC benchmark tools. Allocation counts come from full malloc stack logging, which records every allocation: QUICTransfer -size 1200, per message 24.37 -> 24.34 QUICTransfer, per 500 KB transfer 6,187.6 -> 6,182.4 QUICHandshake, per connection 1,947.4 -> 1,892.0 QUICStreamLoad, per stream 111.8 -> 112.0 The expansions themselves still allocate inside CryptoKit. Wall-clock time comes from running `main`, this change and the other changes measured alongside it in a rotating order for 9 rounds, and comparing each run with `main`'s in the same round. Changes moved paths they do not touch by up to about 1.3%, so differences that size count as noise. Nothing changed beyond noise. --- Sources/SwiftNetwork/QUIC/Protector.swift | 74 ++++++++++++++--------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/Protector.swift b/Sources/SwiftNetwork/QUIC/Protector.swift index 649f5cbe..406022e0 100644 --- a/Sources/SwiftNetwork/QUIC/Protector.swift +++ b/Sources/SwiftNetwork/QUIC/Protector.swift @@ -647,28 +647,40 @@ struct Protector: ~Copyable, PrefixedLoggable { deriveInitialSecrets(destinationCID: destinationCID) } - private func encode(label: String, secretLength: Int) -> [UInt8] { + /// Encodes the HKDF label for `label` and passes it to `body`. + /// + /// The encoding only has to last for one expansion, so it is built in temporary storage rather than an array. + private func withEncodedLabel( + _ label: String, + secretLength: Int, + _ body: (UnsafeRawBufferPointer) -> Result + ) -> Result { let quicLabel = "tls13 " let labelLength = quicLabel.utf8.count + label.utf8.count // 2 is for the length, 1 byte prefix for each label, 1 byte for context let totalLength = 2 + 1 + labelLength + 1 - var result = [UInt8](repeating: 0, count: totalLength) - var index = 0 - - // Encode the length of the secret - result[index] = UInt8((secretLength >> 8) & 0xff) - index += 1 - result[index] = UInt8(secretLength & 0xff) - index += 1 - result[index] = UInt8(labelLength) - index += 1 - result.replaceSubrange(index..> 8) & 0xff) + index += 1 + result[index] = UInt8(secretLength & 0xff) + index += 1 + result[index] = UInt8(labelLength) + index += 1 + for byte in quicLabel.utf8 { + result[index] = byte + index += 1 + } + for byte in label.utf8 { + result[index] = byte + index += 1 + } + result[index] = 0 + + return body(UnsafeRawBufferPointer(result)) + } } private func deriveWithSHA256( @@ -676,12 +688,13 @@ struct Protector: ~Copyable, PrefixedLoggable { label: String, outputSecretLength: Int ) -> SymmetricKey { - let encodedLabel = encode(label: label, secretLength: outputSecretLength) - return HKDF.expand( - pseudoRandomKey: inputSecret, - info: encodedLabel, - outputByteCount: outputSecretLength - ) + withEncodedLabel(label, secretLength: outputSecretLength) { encodedLabel in + HKDF.expand( + pseudoRandomKey: inputSecret, + info: encodedLabel, + outputByteCount: outputSecretLength + ) + } } private func deriveWithSHA384( @@ -689,12 +702,13 @@ struct Protector: ~Copyable, PrefixedLoggable { label: String, outputSecretLength: Int ) -> SymmetricKey { - let encodedLabel = encode(label: label, secretLength: outputSecretLength) - return HKDF.expand( - pseudoRandomKey: inputSecret, - info: encodedLabel, - outputByteCount: outputSecretLength - ) + withEncodedLabel(label, secretLength: outputSecretLength) { encodedLabel in + HKDF.expand( + pseudoRandomKey: inputSecret, + info: encodedLabel, + outputByteCount: outputSecretLength + ) + } } mutating func deriveInitialSecrets(destinationCID: QUICConnectionID) { From 44c216c45b7f7d5b634294fb9323a118a591a2a3 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Fri, 2 Oct 2026 12:18:56 -0400 Subject: [PATCH 2/2] Build HKDF labels in an `InlineArray` TLS caps an HKDF label at 255 bytes, so the encoding fits a fixed 259-byte inline array, and building it no longer writes through an unsafe buffer: every store is bounds-checked. Only handing the finished label to `HKDF.expand`, which takes `DataProtocol`, still reads it through a scoped unsafe buffer. The allocation count is unchanged. --- Sources/SwiftNetwork/QUIC/Protector.swift | 42 +++++++++++++---------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/Protector.swift b/Sources/SwiftNetwork/QUIC/Protector.swift index 406022e0..fde7fd7e 100644 --- a/Sources/SwiftNetwork/QUIC/Protector.swift +++ b/Sources/SwiftNetwork/QUIC/Protector.swift @@ -649,7 +649,7 @@ struct Protector: ~Copyable, PrefixedLoggable { /// Encodes the HKDF label for `label` and passes it to `body`. /// - /// The encoding only has to last for one expansion, so it is built in temporary storage rather than an array. + /// The encoding only has to last for one expansion, so it is built in an inline array rather than on the heap. private func withEncodedLabel( _ label: String, secretLength: Int, @@ -657,29 +657,33 @@ struct Protector: ~Copyable, PrefixedLoggable { ) -> Result { let quicLabel = "tls13 " let labelLength = quicLabel.utf8.count + label.utf8.count + // TLS caps a label at 255 bytes (RFC 8446 Section 7.1), so the encoding is at most 259: 2 bytes of length, + // a 1-byte label length, the label, and a 1-byte length for the empty context. + precondition(labelLength <= 255, "HKDF label is longer than TLS allows") // 2 is for the length, 1 byte prefix for each label, 1 byte for context let totalLength = 2 + 1 + labelLength + 1 - return withUnsafeTemporaryAllocation(byteCount: totalLength, alignment: 1) { result in - var index = 0 - - // Encode the length of the secret - result[index] = UInt8((secretLength >> 8) & 0xff) - index += 1 - result[index] = UInt8(secretLength & 0xff) + var result = InlineArray<259, UInt8>(repeating: 0) + var index = 0 + + // Encode the length of the secret + result[index] = UInt8((secretLength >> 8) & 0xff) + index += 1 + result[index] = UInt8(secretLength & 0xff) + index += 1 + result[index] = UInt8(labelLength) + index += 1 + for byte in quicLabel.utf8 { + result[index] = byte index += 1 - result[index] = UInt8(labelLength) + } + for byte in label.utf8 { + result[index] = byte index += 1 - for byte in quicLabel.utf8 { - result[index] = byte - index += 1 - } - for byte in label.utf8 { - result[index] = byte - index += 1 - } - result[index] = 0 + } + result[index] = 0 - return body(UnsafeRawBufferPointer(result)) + return result.span.withUnsafeBytes { bytes in + body(UnsafeRawBufferPointer(rebasing: bytes[..