-
Notifications
You must be signed in to change notification settings - Fork 11
Fix ISO10126 and PKCS7 unpad #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,4 +103,94 @@ class PaddingTest extends Test { | |
| eq(plainText[i], padTbc.toHex().toUpperCase()); | ||
| } | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------- | ||
| // Security regression tests | ||
| // ----------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * PKCS7.unpad must reject all invalid inputs with a single uniform | ||
| * exception, regardless of *why* validation fails. Using different | ||
| * code paths for "out of range" vs "wrong byte value" leaks the | ||
| * padding length to an attacker (padding oracle / timing side-channel). | ||
| */ | ||
| public function test_pkcs7_unpad_security():Void { | ||
| trace("PKCS7 security: all invalid inputs must throw ..."); | ||
|
|
||
| // padding = 0 is never produced by pad() and must be rejected | ||
| exc(() -> PKCS7.unpad(Bytes.ofHex("0000000000000000"))); | ||
|
|
||
| // padding byte value (0xFF = 255) exceeds the message length (1 byte) | ||
| exc(() -> PKCS7.unpad(Bytes.ofHex("FF"))); | ||
|
|
||
| // padding byte value exceeds the message length (4 bytes, value 8) | ||
| exc(() -> PKCS7.unpad(Bytes.ofHex("04040408"))); | ||
|
|
||
| // one corrupted byte in the middle of an otherwise valid padding region | ||
| // "0001040404040404": last byte = 4, bytes at positions 4-7 are 00,04,04,04 | ||
| exc(() -> PKCS7.unpad(Bytes.ofHex("0001040404040404"))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it is straight up lying about the test case: the bytes should be (for example) |
||
|
|
||
| // only the first padding byte is wrong — catches a non-constant-time loop | ||
| // "0808080808080801": last byte = 1, byte at position 7 = 0x01 — that's fine, | ||
| // but the declared padding value is 1, so only position 7 is checked and it | ||
| // equals 1 → actually valid. Use a case where the first mismatch is early: | ||
| // "0102030404040404": last byte = 4, bytes at positions 4-7: 04,04,04,04 → valid | ||
| // "0102030400040404": last byte = 4, byte at position 4 = 0x00 ≠ 4 → invalid | ||
| exc(() -> PKCS7.unpad(Bytes.ofHex("0102030400040404"))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LLMism of generating an invalid test case, realising it's fine, then trying again. This is also the same test case in the end as the previous one, remove. |
||
|
|
||
| trace("PKCS7 security: valid inputs must still be accepted ..."); | ||
|
|
||
| // full block of padding (blockSize = 8, padding = 8) | ||
| var result = PKCS7.unpad(Bytes.ofHex("0808080808080808")); | ||
| eq(0, result.length); | ||
|
|
||
| // single padding byte | ||
| eq("01", PKCS7.unpad(Bytes.ofHex("0101")).toHex().toUpperCase()); | ||
|
|
||
| // round-trip: pad then unpad must yield the original plaintext | ||
| for (i in 0...plainText.length) { | ||
| var original = Bytes.ofHex(plainText[i]); | ||
| var padded = PKCS7.pad(original, BLOCK_SIZE); | ||
| var restored = PKCS7.unpad(padded); | ||
| eq(plainText[i], restored.toHex().toUpperCase()); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * ISO10126.unpad must validate the padding length field before using it. | ||
| * Without this check an attacker can supply a crafted last byte that | ||
| * causes an out-of-bounds access, crashing the program. | ||
| * | ||
| * ISO 10126 pad() always writes a length-byte ≥ 1, so 0 is never a | ||
| * legitimate value and must also be rejected. | ||
| */ | ||
| public function test_iso10126_unpad_security():Void { | ||
| trace("ISO10126 security: padding = 0 must be rejected ..."); | ||
| // Last byte 0x00: never produced by pad(), must be rejected | ||
| exc(() -> ISO10126.unpad(Bytes.ofHex("0100"))); | ||
|
|
||
| trace("ISO10126 security: padding > length must not crash ..."); | ||
| // 1-byte input, last byte = 0xFF (255) — would read sub(0, -254) | ||
| exc(() -> ISO10126.unpad(Bytes.ofHex("FF"))); | ||
|
|
||
| // 4-byte input, last byte = 0x10 (16 > 4) — padding larger than message | ||
| exc(() -> ISO10126.unpad(Bytes.ofHex("AABBCC10"))); | ||
|
|
||
| trace("ISO10126 security: valid inputs must still be accepted ..."); | ||
|
|
||
| // Entire buffer is padding: 5 bytes, last byte = 5, result is empty | ||
| var result = ISO10126.unpad(Bytes.ofHex("AABBCCDD05")); | ||
| eq(0, result.length); | ||
|
|
||
| // Single padding byte: last byte = 1 | ||
| eq("01", ISO10126.unpad(Bytes.ofHex("0101")).toHex().toUpperCase()); | ||
|
|
||
| // Round-trip: pad then unpad must yield the original plaintext | ||
| for (i in 0...plainText.length) { | ||
| var original = Bytes.ofHex(plainText[i]); | ||
| var padded = ISO10126.pad(original, BLOCK_SIZE); | ||
| var restored = ISO10126.unpad(padded); | ||
| eq(plainText[i], restored.toHex().toUpperCase()); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine but assumes 32-bit integers. At least Python and JS are failing some of the tests because the shift here does not produce the expected bit pattern.
haxe.Int32would probably fix this?