Skip to content

Commit af50020

Browse files
authored
Add variable-length output support to SHAKE digests (#23)
SHAKE128 and SHAKE256 are XOFs that can produce arbitrary-length output, but the constructors hardcoded fixed sizes (16 and 32 bytes). The infrastructure for variable length already existed (_variable_length flag, EVP_DigestFinalXOF dispatch in final()) but was inaccessible. Add a size parameter to both constructors with defaults matching the previous fixed values. On OpenSSL 1.1.x, the parameter is accepted but ignored (the default is always used) to prevent buffer overflow from EVP_DigestFinal_ex writing a fixed-size output. Four property tests (OpenSSL 3.0.x only) verify output length matches the requested size and that the XOF prefix property holds: a shorter output is a prefix of a longer output from the same input. Closes #6
1 parent 94d1db1 commit af50020

4 files changed

Lines changed: 127 additions & 12 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## Add variable-length output support to SHAKE digests
2+
3+
The `shake128` and `shake256` constructors on `Digest` now accept an optional `size` parameter that controls the output length in bytes. The defaults match the previous fixed sizes (16 bytes for SHAKE128, 32 for SHAKE256), so existing code is unaffected.
4+
5+
Variable-length output requires OpenSSL 3.0.x. On OpenSSL 1.1.x, the default size is always used regardless of the parameter value.
6+
7+
```pony
8+
// 64-byte SHAKE256 digest (OpenSSL 3.0.x)
9+
let d = Digest.shake256(64)
10+
d.append("input data")?
11+
let hash: Array[U8] val = d.final() // 64 bytes
12+
```

examples/digest-example/digest-example.pony

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,16 @@ actor Main
1313
else
1414
env.out.print("Error computing hash")
1515
end
16+
17+
// SHAKE256 with variable-length output (OpenSSL 3.0.x only)
18+
ifdef "openssl_3.0.x" then
19+
let shake: Digest = Digest.shake256(64)
20+
try
21+
shake.append("Hello ")?
22+
shake.append("World")?
23+
let shake_hash: Array[U8] val = shake.final()
24+
env.out.print("SHAKE256 (64 bytes): " + ToHexString(shake_hash))
25+
else
26+
env.out.print("Error computing SHAKE hash")
27+
end
28+
end

ssl/crypto/_test.pony

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ actor \nodoc\ Main is TestList
2929
test(Property1UnitTest[USize](_TestPbkdf2Sha256OutputLength))
3030
test(Property1UnitTest[USize](_TestPbkdf2Sha256Deterministic))
3131
end
32+
ifdef "openssl_3.0.x" then
33+
test(Property1UnitTest[USize](_TestShake128OutputLength))
34+
test(Property1UnitTest[USize](_TestShake256OutputLength))
35+
test(Property1UnitTest[USize](_TestShake128XofPrefix))
36+
test(Property1UnitTest[USize](_TestShake256XofPrefix))
37+
end
3238

3339
class \nodoc\ iso _TestConstantTimeCompare is UnitTest
3440
fun name(): String => "crypto/ConstantTimeCompare"
@@ -430,3 +436,75 @@ class \nodoc\ iso _TestRandBytesNonConstant is Property1[USize]
430436
let a = RandBytes(sample)?
431437
let b = RandBytes(sample)?
432438
h.assert_false(ConstantTimeCompare(a, b))
439+
440+
class \nodoc\ iso _TestShake128OutputLength is Property1[USize]
441+
fun name(): String => "crypto/Shake128/property/output_length"
442+
443+
fun gen(): Generator[USize] =>
444+
Generators.usize(1, 256)
445+
446+
fun ref property(sample: USize, h: PropertyHelper) ? =>
447+
ifdef "openssl_3.0.x" then
448+
let d = Digest.shake128(sample)
449+
d.append("test")?
450+
h.assert_eq[USize](sample, d.final().size())
451+
end
452+
453+
class \nodoc\ iso _TestShake256OutputLength is Property1[USize]
454+
fun name(): String => "crypto/Shake256/property/output_length"
455+
456+
fun gen(): Generator[USize] =>
457+
Generators.usize(1, 256)
458+
459+
fun ref property(sample: USize, h: PropertyHelper) ? =>
460+
ifdef "openssl_3.0.x" then
461+
let d = Digest.shake256(sample)
462+
d.append("test")?
463+
h.assert_eq[USize](sample, d.final().size())
464+
end
465+
466+
class \nodoc\ iso _TestShake128XofPrefix is Property1[USize]
467+
fun name(): String => "crypto/Shake128/property/xof_prefix"
468+
469+
fun gen(): Generator[USize] =>
470+
Generators.usize(2, 256)
471+
472+
fun ref property(sample: USize, h: PropertyHelper) ? =>
473+
ifdef "openssl_3.0.x" then
474+
let small_size = sample / 2
475+
let large_size = sample
476+
477+
let small = Digest.shake128(small_size)
478+
small.append("test input")?
479+
let small_result = small.final()
480+
481+
let large = Digest.shake128(large_size)
482+
large.append("test input")?
483+
let large_result = large.final()
484+
485+
h.assert_array_eq[U8](small_result,
486+
large_result.trim(0, small_size))
487+
end
488+
489+
class \nodoc\ iso _TestShake256XofPrefix is Property1[USize]
490+
fun name(): String => "crypto/Shake256/property/xof_prefix"
491+
492+
fun gen(): Generator[USize] =>
493+
Generators.usize(2, 256)
494+
495+
fun ref property(sample: USize, h: PropertyHelper) ? =>
496+
ifdef "openssl_3.0.x" then
497+
let small_size = sample / 2
498+
let large_size = sample
499+
500+
let small = Digest.shake256(small_size)
501+
small.append("test input")?
502+
let small_result = small.final()
503+
504+
let large = Digest.shake256(large_size)
505+
large.append("test input")?
506+
let large_result = large.final()
507+
508+
h.assert_array_eq[U8](small_result,
509+
large_result.trim(0, small_size))
510+
end

ssl/crypto/digest.pony

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,33 +124,45 @@ class Digest
124124
end
125125
@EVP_DigestInit_ex(_ctx, @EVP_sha512(), USize(0))
126126

127-
new shake128() =>
127+
new shake128(size': USize = 16) =>
128128
"""
129-
Use the Shake128 algorithm to calculate the hash.
129+
Use the SHAKE128 algorithm to calculate the hash.
130+
131+
SHAKE128 is an extendable output function (XOF) that can produce
132+
variable-length output. The `size'` parameter controls the output length
133+
in bytes (default: 16). Variable-length output requires OpenSSL 3.0.x;
134+
on OpenSSL 1.1.x, the default size is always used.
130135
"""
131-
_digest_size = 16
132136
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
133-
ifdef not "openssl_3.0.x" then
134-
_variable_length = false
135-
else
137+
ifdef "openssl_3.0.x" then
136138
_variable_length = true
139+
_digest_size = size'
140+
else
141+
_variable_length = false
142+
_digest_size = 16
137143
end
138144
_ctx = @EVP_MD_CTX_new()
139145
@EVP_DigestInit_ex(_ctx, @EVP_shake128(), USize(0))
140146
else
141147
compile_error "shake128 is only supported with OpenSSL 1.1.x or 3.0.x"
142148
end
143149

144-
new shake256() =>
150+
new shake256(size': USize = 32) =>
145151
"""
146-
Use the Shake256 algorithm to calculate the hash.
152+
Use the SHAKE256 algorithm to calculate the hash.
153+
154+
SHAKE256 is an extendable output function (XOF) that can produce
155+
variable-length output. The `size'` parameter controls the output length
156+
in bytes (default: 32). Variable-length output requires OpenSSL 3.0.x;
157+
on OpenSSL 1.1.x, the default size is always used.
147158
"""
148-
_digest_size = 32
149159
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
150-
ifdef not "openssl_3.0.x" then
151-
_variable_length = false
152-
else
160+
ifdef "openssl_3.0.x" then
153161
_variable_length = true
162+
_digest_size = size'
163+
else
164+
_variable_length = false
165+
_digest_size = 32
154166
end
155167
_ctx = @EVP_MD_CTX_new()
156168
@EVP_DigestInit_ex(_ctx, @EVP_shake256(), USize(0))

0 commit comments

Comments
 (0)