Skip to content

Commit 3e3cb89

Browse files
committed
Add OpenSSL 4.0.x support
OpenSSL 4.0.0 was released in April 2026, and a shared-docker builder image (`shared-docker-ci-standard-builder-with-openssl-4.0.0`) is now available. The builder configures OpenSSL with `--api=3.0.0`, preserving the 3.0 application API — so extending every `openssl_3.0.x` compile guard to also list `openssl_4.0.x` is sufficient to support the new backend at the FFI boundary. Also drops "supported on..." docstrings that listed every supported backend — tautological prose that's better omitted. Version qualifiers remain where they express a real subset (SHAKE variable-length output). Incidentally corrects a pre-existing docstring in `ssl/crypto/crypto.pony` that listed PBKDF2 as requiring OpenSSL only, though LibreSSL has been supported since first-class LibreSSL landed. Closes #44
1 parent 6b8cd17 commit 3e3cb89

15 files changed

Lines changed: 108 additions & 68 deletions

File tree

.github/workflows/breakage-against-linux-ponyc-latest.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,24 @@ jobs:
9292
type: stream
9393
topic: ${{ github.repository }} scheduled job failure
9494
content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed.
95+
96+
test-openssl-4-vs-ponyc-latest:
97+
name: OpenSSL 4.x with ponyc main
98+
runs-on: ubuntu-latest
99+
container:
100+
image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-openssl-4.0.0:nightly
101+
steps:
102+
- uses: actions/checkout@v6.0.2
103+
- name: Test
104+
run: make test config=debug ssl=4.0.x
105+
- name: Send alert on failure
106+
if: ${{ failure() }}
107+
uses: zulip/github-actions-zulip/send-message@e4c8f27c732ba9bd98ac6be0583096dea82feea5
108+
with:
109+
api-key: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_API_KEY }}
110+
email: ${{ secrets.ZULIP_SCHEDULED_JOB_FAILURE_EMAIL }}
111+
organization-url: 'https://ponylang.zulipchat.com/'
112+
to: notifications
113+
type: stream
114+
topic: ${{ github.repository }} scheduled job failure
115+
content: ${{ github.server_url}}/${{ github.repository }}/actions/runs/${{ github.run_id }} failed.

.github/workflows/pr.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ jobs:
5757
- name: Test
5858
run: make test config=debug ssl=3.0.x
5959

60+
test-openssl-4-vs-ponyc-release:
61+
name: OpenSSL 4.x with most recent ponyc release
62+
runs-on: ubuntu-latest
63+
container:
64+
image: ghcr.io/ponylang/shared-docker-ci-standard-builder-with-openssl-4.0.0:release
65+
steps:
66+
- uses: actions/checkout@v6.0.2
67+
- name: Test
68+
run: make test config=debug ssl=4.0.x
69+
6070
test-x86-64-windows-vs-ponyc-release:
6171
name: Windows x86-64 (LibreSSL) with most recent ponyc release
6272
runs-on: windows-2025
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Add OpenSSL 4.0.x support
2+
3+
OpenSSL 4.0.x is now a supported backend. Select it at compile time with `-Dopenssl_4.0.x`, or pass `ssl=4.0.x` to `make` when building the library itself. The library's API is unchanged; existing code continues to work without modification.
4+

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ SSL version is **required**. Valid values:
1515

1616
| `ssl=` value | Define passed to ponyc | Backend |
1717
|---|---|---|
18+
| `4.0.x` | `-Dopenssl_4.0.x` | OpenSSL 4.x |
1819
| `3.0.x` | `-Dopenssl_3.0.x` | OpenSSL 3.x |
1920
| `1.1.x` | `-Dopenssl_1.1.x` | OpenSSL 1.1.x |
2021
| `libressl` | `-Dlibressl` | LibreSSL |
@@ -37,12 +38,12 @@ Version-specific code uses two patterns:
3738

3839
**FFI declarations** use `if` guards on the `use` statement:
3940
```pony
40-
use @TLS_method[Pointer[None]]() if "openssl_1.1.x" or "openssl_3.0.x" or "libressl"
41+
use @TLS_method[Pointer[None]]() if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
4142
```
4243

4344
**Code blocks** use `ifdef` with `elseif` chains and a compile_error catch-all:
4445
```pony
45-
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
46+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" then
4647
// OpenSSL path
4748
elseif "libressl" then
4849
// LibreSSL path (when it diverges from OpenSSL)

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ else
2424
endif
2525

2626
ifeq (,$(filter $(MAKECMDGOALS),clean docs realclean TAGS))
27-
ifeq ($(ssl), 3.0.x)
27+
ifeq ($(ssl), 4.0.x)
28+
SSL = -Dopenssl_4.0.x
29+
else ifeq ($(ssl), 3.0.x)
2830
SSL = -Dopenssl_3.0.x
2931
else ifeq ($(ssl), 1.1.x)
3032
SSL = -Dopenssl_1.1.x

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Production ready.
1717

1818
## Supported SSL versions
1919

20-
OpenSSL 1.1.x, OpenSSL 3.0.x, and LibreSSL are supported. Select the library version at compile-time using Pony's compile time definition functionality.
20+
OpenSSL 1.1.x, OpenSSL 3.0.x, OpenSSL 4.0.x, and LibreSSL are supported. Select the library version at compile-time using Pony's compile time definition functionality.
2121

2222
### Using OpenSSL 1.1.x
2323

@@ -31,6 +31,12 @@ corral run -- ponyc -Dopenssl_1.1.x
3131
corral run -- ponyc -Dopenssl_3.0.x
3232
```
3333

34+
### Using OpenSSL 4.0.x
35+
36+
```bash
37+
corral run -- ponyc -Dopenssl_4.0.x
38+
```
39+
3440
### Using LibreSSL
3541

3642
```bash

examples/digest-example/digest-example.pony

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ actor Main
1414
env.out.print("Error computing hash")
1515
end
1616

17-
// SHAKE256 with variable-length output (OpenSSL 3.0.x only)
18-
ifdef "openssl_3.0.x" then
17+
// SHAKE256 with variable-length output (OpenSSL 3.0.x or 4.0.x)
18+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
1919
let shake: Digest = Digest.shake256(64)
2020
try
2121
shake.append("Hello ")?

ssl/crypto/_test.pony

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ actor \nodoc\ Main is TestList
2323
test(Property1UnitTest[USize](_TestHmacSha256Deterministic))
2424
test(Property1UnitTest[USize](_TestRandBytesOutputLength))
2525
test(Property1UnitTest[USize](_TestRandBytesNonConstant))
26-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
26+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
2727
test(_TestPbkdf2Sha256Rfc7914)
2828
test(_TestPbkdf2Sha256Scram)
2929
test(Property1UnitTest[USize](_TestPbkdf2Sha256OutputLength))
3030
test(Property1UnitTest[USize](_TestPbkdf2Sha256Deterministic))
3131
end
32-
ifdef "openssl_3.0.x" then
32+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
3333
test(Property1UnitTest[USize](_TestShake128OutputLength))
3434
test(Property1UnitTest[USize](_TestShake256OutputLength))
3535
test(Property1UnitTest[USize](_TestShake128XofPrefix))
@@ -179,7 +179,7 @@ class \nodoc\ iso _TestDigest is UnitTest
179179
"cd945c6a77006272322f911c9be31fa970043daa4b61cee607566cbfa2c69b09",
180180
ToHexString(sha512.final()))
181181

182-
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
182+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" then
183183
let shake128 = Digest.shake128()
184184
shake128.append("message1")?
185185
shake128.append("message2")?
@@ -326,7 +326,7 @@ class \nodoc\ iso _TestPbkdf2Sha256Rfc7914 is UnitTest
326326
fun name(): String => "crypto/Pbkdf2Sha256/RFC7914"
327327

328328
fun apply(h: TestHelper) ? =>
329-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
329+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
330330
// Test Vector 1: Password "passwd", Salt "salt", c=1, dkLen=64
331331
h.assert_eq[String](
332332
"55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc" +
@@ -349,7 +349,7 @@ class \nodoc\ iso _TestPbkdf2Sha256Scram is UnitTest
349349
fun name(): String => "crypto/Pbkdf2Sha256/SCRAM"
350350

351351
fun apply(h: TestHelper) ? =>
352-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
352+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
353353
// Salt: decoded bytes of base64 "W22ZaJ0SNY7soEsUEjb6gQ=="
354354
let salt = recover val [as U8:
355355
0x5b; 0x6d; 0x99; 0x68; 0x9d; 0x12; 0x35; 0x8e
@@ -408,7 +408,7 @@ class \nodoc\ iso _TestPbkdf2Sha256OutputLength is Property1[USize]
408408
Generators.usize(1, 128)
409409

410410
fun ref property(sample: USize, h: PropertyHelper) ? =>
411-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
411+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
412412
h.assert_eq[USize](sample,
413413
Pbkdf2Sha256("p", "s", 1, sample)?.size())
414414
end
@@ -420,7 +420,7 @@ class \nodoc\ iso _TestPbkdf2Sha256Deterministic is Property1[USize]
420420
Generators.usize(1, 64)
421421

422422
fun ref property(sample: USize, h: PropertyHelper) ? =>
423-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
423+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
424424
h.assert_array_eq[U8](
425425
Pbkdf2Sha256("p", "s", 1, sample)?,
426426
Pbkdf2Sha256("p", "s", 1, sample)?)
@@ -453,7 +453,7 @@ class \nodoc\ iso _TestShake128OutputLength is Property1[USize]
453453
Generators.usize(1, 256)
454454

455455
fun ref property(sample: USize, h: PropertyHelper) ? =>
456-
ifdef "openssl_3.0.x" then
456+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
457457
let d = Digest.shake128(sample)
458458
d.append("test")?
459459
h.assert_eq[USize](sample, d.final().size())
@@ -466,7 +466,7 @@ class \nodoc\ iso _TestShake256OutputLength is Property1[USize]
466466
Generators.usize(1, 256)
467467

468468
fun ref property(sample: USize, h: PropertyHelper) ? =>
469-
ifdef "openssl_3.0.x" then
469+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
470470
let d = Digest.shake256(sample)
471471
d.append("test")?
472472
h.assert_eq[USize](sample, d.final().size())
@@ -479,7 +479,7 @@ class \nodoc\ iso _TestShake128XofPrefix is Property1[USize]
479479
Generators.usize(2, 256)
480480

481481
fun ref property(sample: USize, h: PropertyHelper) ? =>
482-
ifdef "openssl_3.0.x" then
482+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
483483
let small_size = sample / 2
484484
let large_size = sample
485485

@@ -502,7 +502,7 @@ class \nodoc\ iso _TestShake256XofPrefix is Property1[USize]
502502
Generators.usize(2, 256)
503503

504504
fun ref property(sample: USize, h: PropertyHelper) ? =>
505-
ifdef "openssl_3.0.x" then
505+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
506506
let small_size = sample / 2
507507
let large_size = sample
508508

ssl/crypto/crypto.pony

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The crypto package provides cryptographic primitives built on OpenSSL:
44
* One-shot hash functions (`MD5`, `SHA256`, etc.) and streaming digests
55
(`Digest`)
66
* HMAC message authentication (`HmacSha256`)
7-
* PBKDF2 key derivation (`Pbkdf2Sha256`, requires OpenSSL 1.1.x or 3.0.x)
7+
* PBKDF2 key derivation (`Pbkdf2Sha256`)
88
* Cryptographically secure random bytes (`RandBytes`)
99
* Constant-time comparison (`ConstantTimeCompare`)
1010
"""

ssl/crypto/digest.pony

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ use "path:/opt/homebrew/opt/libressl/lib" if osx and arm
33
use "lib:crypto"
44
use "lib:bcrypt" if windows
55

6-
use @EVP_MD_CTX_new[Pointer[_EVPCTX]]() if "openssl_1.1.x" or "openssl_3.0.x" or "libressl"
6+
use @EVP_MD_CTX_new[Pointer[_EVPCTX]]() if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
77
use @EVP_DigestInit_ex[I32](ctx: Pointer[_EVPCTX] tag, t: Pointer[_EVPMD], impl: USize)
88
use @EVP_DigestUpdate[I32](ctx: Pointer[_EVPCTX] tag, d: Pointer[U8] tag, cnt: USize)
99
use @EVP_DigestFinal_ex[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, s: Pointer[USize])
10-
use @EVP_DigestFinalXOF[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, len: USize) if "openssl_3.0.x"
11-
use @EVP_MD_CTX_free[None](ctx: Pointer[_EVPCTX]) if "openssl_1.1.x" or "openssl_3.0.x" or "libressl"
10+
use @EVP_DigestFinalXOF[I32](ctx: Pointer[_EVPCTX] tag, md: Pointer[U8] tag, len: USize) if "openssl_3.0.x" or "openssl_4.0.x"
11+
use @EVP_MD_CTX_free[None](ctx: Pointer[_EVPCTX]) if "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl"
1212

1313
use @EVP_md5[Pointer[_EVPMD]]()
1414
use @EVP_ripemd160[Pointer[_EVPMD]]()
@@ -39,7 +39,7 @@ class Digest
3939
"""
4040
_variable_length = false
4141
_digest_size = 16
42-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
42+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
4343
_ctx = @EVP_MD_CTX_new()
4444
else
4545
compile_error "You must select an SSL version to use."
@@ -52,7 +52,7 @@ class Digest
5252
"""
5353
_variable_length = false
5454
_digest_size = 20
55-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
55+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
5656
_ctx = @EVP_MD_CTX_new()
5757
else
5858
compile_error "You must select an SSL version to use."
@@ -65,7 +65,7 @@ class Digest
6565
"""
6666
_variable_length = false
6767
_digest_size = 20
68-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
68+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
6969
_ctx = @EVP_MD_CTX_new()
7070
else
7171
compile_error "You must select an SSL version to use."
@@ -78,7 +78,7 @@ class Digest
7878
"""
7979
_variable_length = false
8080
_digest_size = 28
81-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
81+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
8282
_ctx = @EVP_MD_CTX_new()
8383
else
8484
compile_error "You must select an SSL version to use."
@@ -91,7 +91,7 @@ class Digest
9191
"""
9292
_variable_length = false
9393
_digest_size = 32
94-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
94+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
9595
_ctx = @EVP_MD_CTX_new()
9696
else
9797
compile_error "You must select an SSL version to use."
@@ -104,7 +104,7 @@ class Digest
104104
"""
105105
_variable_length = false
106106
_digest_size = 48
107-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
107+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
108108
_ctx = @EVP_MD_CTX_new()
109109
else
110110
compile_error "You must select an SSL version to use."
@@ -117,7 +117,7 @@ class Digest
117117
"""
118118
_variable_length = false
119119
_digest_size = 64
120-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
120+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
121121
_ctx = @EVP_MD_CTX_new()
122122
else
123123
compile_error "You must select an SSL version to use."
@@ -130,11 +130,11 @@ class Digest
130130
131131
SHAKE128 is an extendable output function (XOF) that can produce
132132
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.
133+
in bytes (default: 16). Variable-length output requires OpenSSL 3.0.x or
134+
OpenSSL 4.0.x; on OpenSSL 1.1.x, the default size is always used.
135135
"""
136-
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
137-
ifdef "openssl_3.0.x" then
136+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" then
137+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
138138
_variable_length = true
139139
_digest_size = size'
140140
else
@@ -144,7 +144,7 @@ class Digest
144144
_ctx = @EVP_MD_CTX_new()
145145
@EVP_DigestInit_ex(_ctx, @EVP_shake128(), USize(0))
146146
else
147-
compile_error "shake128 is only supported with OpenSSL 1.1.x or 3.0.x"
147+
compile_error "shake128 is only supported with OpenSSL 1.1.x, 3.0.x, or 4.0.x"
148148
end
149149

150150
new shake256(size': USize = 32) =>
@@ -153,11 +153,11 @@ class Digest
153153
154154
SHAKE256 is an extendable output function (XOF) that can produce
155155
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.
156+
in bytes (default: 32). Variable-length output requires OpenSSL 3.0.x or
157+
OpenSSL 4.0.x; on OpenSSL 1.1.x, the default size is always used.
158158
"""
159-
ifdef "openssl_1.1.x" or "openssl_3.0.x" then
160-
ifdef "openssl_3.0.x" then
159+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" then
160+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
161161
_variable_length = true
162162
_digest_size = size'
163163
else
@@ -167,7 +167,7 @@ class Digest
167167
_ctx = @EVP_MD_CTX_new()
168168
@EVP_DigestInit_ex(_ctx, @EVP_shake256(), USize(0))
169169
else
170-
compile_error "shake256 is only supported with OpenSSL 1.1.x or 3.0.x"
170+
compile_error "shake256 is only supported with OpenSSL 1.1.x, 3.0.x, or 4.0.x"
171171
end
172172

173173
fun ref append(input: ByteSeq) ? =>
@@ -193,13 +193,13 @@ class Digest
193193
if not _variable_length then
194194
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize])
195195
else
196-
ifdef "openssl_3.0.x" then
196+
ifdef "openssl_3.0.x" or "openssl_4.0.x" then
197197
@EVP_DigestFinalXOF(_ctx, digest.cpointer(), size)
198198
else
199199
@EVP_DigestFinal_ex(_ctx, digest.cpointer(), Pointer[USize])
200200
end
201201
end
202-
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "libressl" then
202+
ifdef "openssl_1.1.x" or "openssl_3.0.x" or "openssl_4.0.x" or "libressl" then
203203
@EVP_MD_CTX_free(_ctx)
204204
else
205205
compile_error "You must select an SSL version to use."

0 commit comments

Comments
 (0)