Courier vectors: use a real deposit frame and pin every byte

Both changes address the Codex review on #1472.

The signing vector used type 0x2a, which is not a MessageType on main — it is
the spray-receipt opcode from a separate unmerged branch, so the frame decoded
to nothing and was misleading as an end-to-end courier vector. It now encodes
an actual courierEnvelope deposit: type 0x04 carrying the encoded envelope
from the TLV vector as its payload. 14 header + 8 sender + 8 recipient + 74
envelope = 104 unsigned and unpadded, 256 signed, 168 on the wire. The
envelope is under the 100-byte compression threshold, so the compressed path
stays out of it.

The pre-image assertions checked only length, the first three bytes, the flags
byte and the padding suffix. A change to timestamp encoding, payload length or
field layout could have left the test green while the JSON went stale — which
made the claim that these cannot drift without a test failing untrue as
written. Both the unpadded and padded sequences are now compared in full.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ecgang 2026-07-25 15:29:52 -07:00
parent 0a5647b3e8
commit 6a675ab4fc
2 changed files with 59 additions and 32 deletions

View File

@ -93,18 +93,19 @@
"and nothing in any log points at the cause."
],
"packet": {
"_comment": "A real courierEnvelope deposit frame: type 0x04 carrying the encoded envelope from envelopeTLV.encoded above as its payload.",
"version": 1,
"type": "0x2a",
"type": "0x04",
"ttlOnWire": 7,
"payload": "bb85dcc4d8b17377c61817992df95826"
"payloadIs": "envelopeTLV.encoded (74 bytes)"
},
"unsignedUnpaddedLength": 46,
"unsignedUnpadded": "012a07000001977420dc00010010112233445566778899aabbccddeeff00bb85dcc4d8b17377c61817992df95826",
"unsignedUnpaddedLength": 104,
"unsignedUnpadded": "010407000001977420dc0001004a112233445566778899aabbccddeeff00010010000102030405060708090a0b0c0d0e0f020008000001a3185c500003001e636f75726965722d766563746f722d636970686572746578742d303030310400010405000411223344",
"signingPreimageLength": 256,
"signingPreimagePadByte": "0xd2",
"signingPreimagePadCount": 210,
"signingPreimage": "012a00000001977420dc00010010112233445566778899aabbccddeeff00bb85dcc4d8b17377c61817992df95826d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2",
"signedWireLength": 110,
"signingPreimagePadByte": "0x98",
"signingPreimagePadCount": 152,
"signingPreimage": "010400000001977420dc0001004a112233445566778899aabbccddeeff00010010000102030405060708090a0b0c0d0e0f020008000001a3185c500003001e636f75726965722d766563746f722d636970686572746578742d3030303104000104050004112233449898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898989898",
"signedWireLength": 168,
"flags": {
"_comment": [
"Flag bits: hasRecipient 0x01, hasSignature 0x02, isCompressed 0x04,",

View File

@ -95,40 +95,66 @@ struct CourierVectorTests {
// MARK: Packet canonicalization the trap worth a vector
/// A real `courierEnvelope` frame: type 0x04 carrying an encoded envelope,
/// which is what a deposit actually puts on the wire.
static func envelopePacket() throws -> BitchatPacket {
let envelope = CourierEnvelope(recipientTag: recipientTag,
expiry: expiryMs,
ciphertext: ciphertext,
copies: 4,
prekeyID: 0x1122_3344)
return BitchatPacket(type: 0x04,
senderID: senderID,
recipientID: recipientID,
timestamp: timestampMs,
payload: try #require(envelope.encode()),
signature: nil,
ttl: 7)
}
/// Every byte of the unsigned frame. 14 header + 8 sender + 8 recipient +
/// 74 envelope = 104. The envelope is under the 100-byte compression
/// threshold, so the compressed path never applies here.
static let unpaddedHex = """
010407000001977420dc0001004a112233445566778899aabbccddeeff000100100001020\
30405060708090a0b0c0d0e0f020008000001a3185c500003001e636f75726965722d76656\
3746f722d636970686572746578742d303030310400010405000411223344
""".replacingOccurrences(of: "\n", with: "")
/// Identical to `unpaddedHex` except `ttl` at offset 2 is zeroed.
static let preimageBodyHex = """
010400000001977420dc0001004a112233445566778899aabbccddeeff000100100001020\
30405060708090a0b0c0d0e0f020008000001a3185c500003001e636f75726965722d76656\
3746f722d636970686572746578742d303030310400010405000411223344
""".replacingOccurrences(of: "\n", with: "")
/// The signed pre-image is **not** the wire bytes. It zeroes `ttl`, clears
/// the `hasSignature` flag, and is PKCS#7-padded to a block boundary.
/// Both sequences are compared in full a change to timestamp encoding,
/// payload length, or field layout has to fail here, otherwise the JSON
/// fixture could go stale while this stayed green.
@Test func signingPreimageIsTTLZeroedAndPadded() throws {
let payload = Data(Self.ciphertext.sha256Hash().prefix(CourierEnvelope.tagLength))
let packet = BitchatPacket(type: 0x2A,
senderID: Self.senderID,
recipientID: Self.recipientID,
timestamp: Self.timestampMs,
payload: payload,
signature: nil,
ttl: 7)
let packet = try Self.envelopePacket()
// Unsigned, unpadded: 14 header + 8 sender + 8 recipient + 16 payload.
let unpadded = try #require(BinaryProtocol.encode(packet, padding: false))
#expect(unpadded.count == 46)
#expect(unpadded.hexEncodedString().hasPrefix("012a07")) // ttl = 7 here
#expect(unpadded.count == 104)
#expect(unpadded.hexEncodedString() == Self.unpaddedHex)
let preimage = try #require(packet.toBinaryDataForSigning())
#expect(preimage.count == 256) // padded, not 46
#expect(preimage.hexEncodedString().hasPrefix("012a00")) // ttl zeroed
#expect(preimage.count == 256)
#expect(preimage.hexEncodedString()
== Self.preimageBodyHex + String(repeating: "98", count: 152))
// 152 == 0x98 == the shortfall to the 256-byte block, and every pad
// byte equals it.
#expect(preimage.dropFirst(104).allSatisfy { $0 == 152 })
#expect(preimage[BinaryProtocol.Offsets.flags] == BinaryProtocol.Flags.hasRecipient)
#expect(preimage.dropFirst(46).allSatisfy { $0 == 210 }) // 210 == 0xD2 == pad length
#expect(unpadded[2] == 7 && preimage[2] == 0) // ttl on wire vs signed
}
/// Ed25519 CryptoKit's `Curve25519.Signing`, not X25519 key agreement.
@Test func signatureOverPreimageVerifies() throws {
let payload = Data(Self.ciphertext.sha256Hash().prefix(CourierEnvelope.tagLength))
let packet = BitchatPacket(type: 0x2A,
senderID: Self.senderID,
recipientID: Self.recipientID,
timestamp: Self.timestampMs,
payload: payload,
signature: nil,
ttl: 7)
let packet = try Self.envelopePacket()
let preimage = try #require(packet.toBinaryDataForSigning())
let key = try Curve25519.Signing.PrivateKey(rawRepresentation: Self.signingSeed)
@ -150,12 +176,12 @@ struct CourierVectorTests {
let signature = a
// On the wire the packet is 110 bytes the 46 above plus a 64-byte
// On the wire the packet is 168 bytes the 104 above plus a 64-byte
// signature and the flags byte now also carries hasSignature.
var signed = packet
signed.signature = Data(signature)
let wire = try #require(BinaryProtocol.encode(signed, padding: false))
#expect(wire.count == 110)
#expect(wire.count == 168)
#expect(wire[BinaryProtocol.Offsets.flags]
== BinaryProtocol.Flags.hasRecipient | BinaryProtocol.Flags.hasSignature)
}