Hi all,
I'd like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.
Problem
PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:
- No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
- No way to run (D)TLS over a transport the application controls.
Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.
Proposal (sketch)
A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:
namespace Openssl;
final class Dtls {
public const int HANDSHAKE_ERROR = -1;
public const int HANDSHAKE_CONTINUE = 0;
public const int HANDSHAKE_FINISHED = 1;
public function __construct(bool $isServer = false,
?string $certificate = null, ?string $privateKey = null) {}
public function getFingerprint(?string $digestAlgo = null): string
{}
public function getPeerFingerprint(?string $digestAlgo = null):
?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length):
string|false {}
}
The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.
Status
I've already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I'm sharing it only to
make the discussion concrete -- I'm very open to changing the shape (this
is the point of asking first), and I'll only open a PR/RFC properly if
there's interest. I can post the branch/PR link on request.
Questions
- Is DTLS something we want in core ext/openssl, or is this better left
to userland/FFI? - Is the object + memory-BIO design the right direction? Should the
constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM? - How should we treat the server-side HelloVerifyRequest cookie
exchange (DoS amplification mitigation) -- required for v1, or
acceptable as future scope with a documented caveat? - (Cc ext/openssl maintainers -- your view would be especially valuable.)
Thanks for any feedback.
Gianfrancesco Aurecchia
Hi Gianfrancesco,
I don't have any expertise of the actual protocol, but do have an opinion on the PHP surface design. I apologise if the below feels too much like bikeshedding...
Should the constructor also accept
OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?
I would say instead of a string. Accepting a string means the constructor has to document the expected format, produce appropriate parse errors, etc; accepting an object reduces that to a standard type check. And if the user already has an object, serialising it and then reparsing it is duplicated effort.
In general, I would prefer a design which used "real" types throughout instead of strings and ints, except where performance is critical:
public const int HANDSHAKE_ERROR = -1; public const int HANDSHAKE_CONTINUE = 0; public const int HANDSHAKE_FINISHED = 1;
These should be cases on an enum; probably unbacked, unless there's some reason applications need to work with these integer codes.
public function getFingerprint(?string $digestAlgo = null): string
Perhaps the supported algorithms could be an enum as well, backed by string.
public function feed(string $datagram): int {}
public function pull(): ?string {}
If $datagram has to be in a specific format, would a lightweight DtlsDatagram object be useful? Again, it would delegate the parsing/validation to a specific factory, and give flexibility for extra ways of creating or reusing material without going via strings and revalidating.
public function read(): string|false {} public function exportKeys(string $label, int $length):string|false {}
If the false case here is an expected condition, like EOF, "?string" might be better. If it's just a signal for unexpected errors, throwing an exception would be better.
Hopefully someone with more knowledge of the problem space can comment more on the core idea.
Thanks,
Rowan Tommins
[IMSoP]
Hi Rowan,
Thanks, this is exactly the kind of API-shaping feedback I was hoping for.
Things I'm happy to take:
- Certificate and key as 'OpenSSLCertificate' / 'OpenSSLAsymmetricKey'
objects rather than PEM strings: yes, that aligns with the rest of
ext/openssl. I'd probably still accept PEM as a convenience, but make the
objects the primary form. - Returning 'null' rather than 'false' for an expected empty result (no
datagram pending, no application data yet): agreed, null is clearer there.
On the enums (the handshake state, and digest selection): I like enums, and
personally I'd reach for them too. My hesitation is that I'd rather not
diverge from how ext/openssl is consolidated today: the extension exposes
this kind of thing as integer constants ('OPENSSL_ALGO_', 'OPENSSL_') and
selects digests by name (string), not through enums. Introducing enums just
for DTLS would create a new convention in one corner of the extension.
Whether ext/openssl should move towards enums more broadly feels like a
maintainer-level decision, and Jakub's feedback would be valuable there; he
may well already be considering something along these lines. I'd keep that
question separate from this proposal, so the DTLS work doesn't hinge on a
wider style change, and for now follow the extension's existing convention.
On a 'DtlsDatagram' value object vs raw strings: here I'd lean towards
keeping raw strings, for two reasons. First, 'feed()' and 'pull()' sit on
the hot path (one call per packet), so wrapping every datagram in an object
adds allocation for what is essentially a byte buffer. Second, binary I/O
across PHP (fread/fwrite, the socket and stream functions, the openssl_*
functions) is consistently done with strings, so a datagram object would be
a bit of an outlier; PHP strings already are the byte-buffer type. I'm open
if there's a concrete benefit I'm missing, for example carrying metadata
alongside the bytes.
On direction: Jakub suggested also exposing a 'dtls://' stream wrapper
alongside the low-level engine, which I think is a good idea. I'm waiting
for his feedback on that before settling the overall shape. Your points
apply to the engine/class surface either way, and I'll fold the agreed ones
in.
Thanks again for the careful read.
Gianfrancesco
Il giorno mer 24 giu 2026 alle ore 23:08 Rowan Tommins [IMSoP] <
imsop.php@rwec.co.uk> ha scritto:
Hi Gianfrancesco,
I don't have any expertise of the actual protocol, but do have an opinion
on the PHP surface design. I apologise if the below feels too much like
bikeshedding...On 24 June 2026 12:45:05 BST, Gianfrancesco Aurecchia <
gianfri.aur@gmail.com> wrote:Should the constructor also accept
OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?I would say instead of a string. Accepting a string means the
constructor has to document the expected format, produce appropriate parse
errors, etc; accepting an object reduces that to a standard type check. And
if the user already has an object, serialising it and then reparsing it is
duplicated effort.In general, I would prefer a design which used "real" types throughout
instead of strings and ints, except where performance is critical:public const int HANDSHAKE_ERROR = -1; public const int HANDSHAKE_CONTINUE = 0; public const int HANDSHAKE_FINISHED = 1;These should be cases on an enum; probably unbacked, unless there's some
reason applications need to work with these integer codes.public function getFingerprint(?string $digestAlgo = null): stringPerhaps the supported algorithms could be an enum as well, backed by
string.public function feed(string $datagram): int {}
public function pull(): ?string {}If $datagram has to be in a specific format, would a lightweight
DtlsDatagram object be useful? Again, it would delegate the
parsing/validation to a specific factory, and give flexibility for extra
ways of creating or reusing material without going via strings and
revalidating.public function read(): string|false {} public function exportKeys(string $label, int $length):string|false {}
If the false case here is an expected condition, like EOF, "?string" might
be better. If it's just a signal for unexpected errors, throwing an
exception would be better.Hopefully someone with more knowledge of the problem space can comment
more on the core idea.Thanks,
Rowan Tommins
[IMSoP]
Hi,
On Wed, Jun 24, 2026 at 1:45 PM Gianfrancesco Aurecchia <
gianfri.aur@gmail.com> wrote:
Hi all,
I'd like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.Problem
PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:
- No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
- No way to run (D)TLS over a transport the application controls.
Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.Proposal (sketch)
A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:namespace Openssl; final class Dtls { public const int HANDSHAKE_ERROR = -1; public const int HANDSHAKE_CONTINUE = 0; public const int HANDSHAKE_FINISHED = 1; public function __construct(bool $isServer = false, ?string $certificate = null, ?string $privateKey = null) {} public function getFingerprint(?string $digestAlgo = null): string{}
public function getPeerFingerprint(?string $digestAlgo = null):
?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length):
string|false {}
}The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.Status
I've already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I'm sharing it only to
make the discussion concrete -- I'm very open to changing the shape (this
is the point of asking first), and I'll only open a PR/RFC properly if
there's interest. I can post the branch/PR link on request.Questions
- Is DTLS something we want in core ext/openssl, or is this better left
to userland/FFI?
I think it can be exposed in ext/openssl but not in this way (see below)
- Is the object + memory-BIO design the right direction? Should the
constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?
I would prefer just stream exposure so dtls:// stream instead. See udp://
for how it would be used. I haven't done any testing with udp streams but
the design should be similar like we have for tls that is build on top of
tcp. It might not be exactly small work to do it cleanly though.
- How should we treat the server-side HelloVerifyRequest cookie
exchange (DoS amplification mitigation) -- required for v1, or
acceptable as future scope with a documented caveat?
we could possibly add some context option for it but haven't thought much
about it.
Kind regards,
Jakub
Hi Jakub,
Thanks a lot for looking at this. Your view as the ext/openssl maintainer
is exactly what I was hoping for.
I'll be honest: I hadn't considered exposing DTLS as a stream wrapper (a
'dtls://' built like 'tls://' is over 'tcp://'). It's a really good idea.
For the common case of a dedicated UDP socket that carries only DTLS,
that's clearly the most idiomatic and discoverable API in PHP, and much
closer to what people already know from 'tls://' than what I proposed.
So rather than seeing it as an alternative, I think we should take the
opportunity and do both, because the two genuinely complement each
other and cover different needs:
- dtls://' stream wrapper: owns a UDP socket, mirrors 'tls://'. The
right default for the simple "one socket, DTLS only" case (CoAP-style,
point-to-point, a DTLS server on its own port, etc.). - Low-level, application-driven engine (memory BIOs, 'feed()'/'pull()'):
for the cases where the application must own packet I/O.
That second layer isn't a stylistic preference; it's a hard requirement of
the motivating use case. In WebRTC, DTLS records are multiplexed with other
protocols on the same UDP 5-tuple: STUN (ICE) and SRTP/SCTP (media/data)
share one socket and are demultiplexed by the application from the first
byte of each datagram (RFC 7983 / RFC 5764). A 'dtls://' stream that owns
its socket can't serve that, since it would swallow the STUN and SRTP/SCTP
packets too, with nowhere for the application to route them. The same holds
for anything that shares a transport with DTLS.
The nice part is that these aren't two separate implementations. The stream
wrapper can be built on top of the same low-level engine: the engine
drives the handshake and the records, and the wrapper just adds a UDP
socket and the familiar stream API around it. One core, two surfaces:
the stream for the 90% simple case (exactly the 'tls://'-style ergonomics
you have in mind), and the driver for the multiplexed or embedded cases the
stream can't reach (WebRTC, and anything where DTLS coexists with other
traffic on one socket).
Does that direction sound right to you? I'm very flexible on the surface
API. Rowan's points about certificate/key objects and enums fit either
shape, and I'll fold them in. The only property I'd want to preserve is the
ability to run DTLS without owning the socket, so the WebRTC case (the main
motivation) stays on the table.
For context, I already have the application-driven engine working
end-to-end against Chrome and Firefox via a pure-PHP WebRTC data channel,
so I can speak to the multiplexing requirement concretely:
https://github.com/GianfriAur/php-webrtc-datachannel
Since the engine is already prototyped, I'm happy to also draft the
'dtls://' stream wrapper on top of it, so we can evaluate both surfaces
against real code rather than in the abstract. If the layered direction
sounds reasonable to you, I'll put together a proof of concept of the
wrapper and share it.
Thanks again, and let me know what you think.
Gianfrancesco
Il giorno mer 24 giu 2026 alle ore 23:44 Jakub Zelenka bukka@php.net ha
scritto:
Hi,
On Wed, Jun 24, 2026 at 1:45 PM Gianfrancesco Aurecchia <
gianfri.aur@gmail.com> wrote:Hi all,
I'd like to gauge interest in adding DTLS support to ext/openssl before
writing a formal RFC.Problem
PHP can speak TLS, but only as a stream wrapper where OpenSSL owns the
socket end to end. There are two gaps:
- No DTLS (RFC 6347, the UDP counterpart of TLS) at all.
- No way to run (D)TLS over a transport the application controls.
Several modern protocols are built on DTLS where the application must own
the packet flow: WebRTC data channels and DTLS-SRTP (RFC 5764), CoAP over
DTLS, SIP media keying, and any case where DTLS records are multiplexed
with other traffic on one UDP port. Today these are only reachable from
PHP through FFI to libssl or an external process. DTLS has been an open
request since 2018 (bug #76629) with no implementation so far.Proposal (sketch)
A small, transport-agnostic DTLS endpoint class whose packet I/O goes
through memory BIOs, so the application moves datagrams in/out itself:namespace Openssl; final class Dtls { public const int HANDSHAKE_ERROR = -1; public const int HANDSHAKE_CONTINUE = 0; public const int HANDSHAKE_FINISHED = 1; public function __construct(bool $isServer = false, ?string $certificate = null, ?string $privateKey = null) {} public function getFingerprint(?string $digestAlgo = null):string {}
public function getPeerFingerprint(?string $digestAlgo = null):
?string {}
public function isHandshakeFinished(): bool {}
public function handshake(): int {} // HANDSHAKE_* constants
public function feed(string $datagram): int {}
public function pull(): ?string {}
public function write(string $data): int {}
public function read(): string|false {}
public function exportKeys(string $label, int $length):
string|false {}
}The application drives the handshake by pumping datagrams between peers
(feed/pull), verifies the peer out of band via getPeerFingerprint(), then
exchanges data with write()/read(). exportKeys() exposes
SSL_export_keying_material (RFC 5705), which is what DTLS-SRTP needs.Status
I've already written a working proof-of-concept against master, with
.phpt tests; the full ext/openssl suite is green. I'm sharing it only to
make the discussion concrete -- I'm very open to changing the shape (this
is the point of asking first), and I'll only open a PR/RFC properly if
there's interest. I can post the branch/PR link on request.Questions
- Is DTLS something we want in core ext/openssl, or is this better left
to userland/FFI?I think it can be exposed in ext/openssl but not in this way (see below)
- Is the object + memory-BIO design the right direction? Should the
constructor also accept OpenSSLCertificate/OpenSSLAsymmetricKey
objects in addition to PEM?I would prefer just stream exposure so dtls:// stream instead. See udp://
for how it would be used. I haven't done any testing with udp streams but
the design should be similar like we have for tls that is build on top of
tcp. It might not be exactly small work to do it cleanly though.
- How should we treat the server-side HelloVerifyRequest cookie
exchange (DoS amplification mitigation) -- required for v1, or
acceptable as future scope with a documented caveat?we could possibly add some context option for it but haven't thought much
about it.Kind regards,
Jakub