Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions src/Opc.Ua.PubSub.Udp/Dtls/DtlsDatagramTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ namespace Opc.Ua.PubSub.Udp.Dtls
/// <summary>
/// DTLS wrapper around the UDP datagram transport for Part 14 §7.3.2.4 unicast PubSub.
/// </summary>
public sealed class DtlsDatagramTransport : IPubSubTransport, IDtlsDatagramChannel
public sealed class DtlsDatagramTransport :
IPubSubTransport,
IDtlsDatagramChannel,
IDtlsAuthenticatedPeerChannel
{
/// <summary>
/// Initializes a new <see cref="DtlsDatagramTransport"/>.
Expand Down Expand Up @@ -82,7 +85,8 @@ public DtlsDatagramTransport(
timeProvider,
udpOptions,
diagnostics,
useConnectedUnicastClient: direction == PubSubTransportDirection.Send);
useConnectedUnicastClient: direction == PubSubTransportDirection.Send,
trackLastSeenUnicastPeer: false);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -118,6 +122,7 @@ public event EventHandler<PubSubTransportStateChangedEventArgs>? StateChanged
public async ValueTask OpenAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
ClearAuthenticatedPeer();
IDtlsContext context = await m_contextFactory.CreateAsync(
m_connection,
Endpoint,
Expand Down Expand Up @@ -163,6 +168,7 @@ public async ValueTask CloseAsync(CancellationToken cancellationToken = default)
{
IDtlsContext? context = m_context;
m_context = null;
ClearAuthenticatedPeer();
context?.Dispose();
await m_innerTransport.CloseAsync(cancellationToken).ConfigureAwait(false);
}
Expand All @@ -186,6 +192,15 @@ public async IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
await foreach (PubSubTransportFrame frame in m_innerTransport.ReceiveAsync(cancellationToken)
.ConfigureAwait(false))
{
if (!IsAuthenticatedPeer(frame.SourceEndpoint))
{
// RFC 9147 §5.1: without a negotiated connection ID, a
// record from another address is not part of this association.
// Filter before record protection so redirected ciphertext
// cannot consume the authenticated peer's replay sequence.
continue;
}

ReadOnlyMemory<byte> payload;
try
{
Expand All @@ -198,7 +213,11 @@ public async IAsyncEnumerable<PubSubTransportFrame> ReceiveAsync(
// silently dropped so a forged datagram cannot tear down the transport.
continue;
}
yield return new PubSubTransportFrame(payload, frame.Topic, frame.ReceivedAt);
yield return new PubSubTransportFrame(
payload,
frame.Topic,
frame.ReceivedAt,
frame.SourceEndpoint);
}
}

Expand All @@ -207,6 +226,7 @@ public async ValueTask DisposeAsync()
{
IDtlsContext? context = m_context;
m_context = null;
ClearAuthenticatedPeer();
context?.Dispose();
await m_innerTransport.DisposeAsync().ConfigureAwait(false);
}
Expand Down Expand Up @@ -239,6 +259,54 @@ async ValueTask<DtlsDatagram> IDtlsDatagramChannel.ReceiveAsync(CancellationToke
throw new InvalidOperationException("DTLS datagram channel closed while waiting for a handshake datagram.");
}

void IDtlsAuthenticatedPeerChannel.SetAuthenticatedPeer(IPEndPoint peer)
{
SetAuthenticatedPeer(peer);
}

private bool IsAuthenticatedPeer(IPEndPoint? source)
{
if (source is null)
{
return false;
}

lock (m_peerLock)
{
return m_authenticatedPeer is not null &&
m_authenticatedPeer.Equals(source);
}
}

private void SetAuthenticatedPeer(IPEndPoint peer)
{
if (peer is null)
{
throw new ArgumentNullException(nameof(peer));
}

lock (m_peerLock)
{
if (m_authenticatedPeer is not null &&
!m_authenticatedPeer.Equals(peer))
{
throw new InvalidOperationException(
"A connection-ID-less DTLS association cannot change its authenticated peer endpoint.");
}

m_authenticatedPeer ??= new IPEndPoint(peer.Address, peer.Port);
m_innerTransport.SetAuthenticatedRemoteEndpoint(m_authenticatedPeer);
}
}

private void ClearAuthenticatedPeer()
{
lock (m_peerLock)
{
m_authenticatedPeer = null;
}
}

/// <summary>
/// PubSub connection descriptor backing the DTLS transport.
/// </summary>
Expand All @@ -247,6 +315,8 @@ async ValueTask<DtlsDatagram> IDtlsDatagramChannel.ReceiveAsync(CancellationToke
private readonly IDtlsContextFactory m_contextFactory;
private readonly ITelemetryContext m_telemetry;
private readonly TimeProvider m_timeProvider;
private readonly Lock m_peerLock = new();
private IDtlsContext? m_context;
private IPEndPoint? m_authenticatedPeer;
}
}
92 changes: 77 additions & 15 deletions src/Opc.Ua.PubSub.Udp/Dtls/DtlsHandshakeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ private async ValueTask ConnectAsync(IDtlsDatagramChannel channel, CancellationT
{
using DtlsEcdheKeyExchange ecdhe = new(Profile.KeyExchangeCurve);
var transcript = new DtlsTranscriptHash(GetHashAlgorithm(Profile.CipherSuite));
IPEndPoint serverPeer = GetPeerEndpoint(channel);
byte[] sessionId = CreateRandom(32);
byte[] cookie = [];
byte[] clientHelloBody = [];
Expand All @@ -169,7 +170,10 @@ private async ValueTask ConnectAsync(IDtlsDatagramChannel channel, CancellationT
clientHelloBody);
await SendFlightAsync(channel, clientHelloFrame, cancellationToken).ConfigureAwait(false);
transcript.Append(clientHelloFrame);
DtlsHandshakeFrame firstFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame firstFrame = await ReceiveFrameAsync(
channel,
serverPeer,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(firstFrame, DtlsHandshakeType.ServerHello);
DtlsServerHello serverHello = DtlsHandshakeCodec.DecodeServerHello(firstFrame.Fragment);
Expand All @@ -194,17 +198,28 @@ private async ValueTask ConnectAsync(IDtlsDatagramChannel channel, CancellationT

byte[] serverHelloHash = transcript.GetHash();
m_keyingContext = new DtlsHandshakeKeyingContext(Profile, sharedSecret, serverHelloHash);
await ReceiveAndAppendAsync(channel, transcript, DtlsHandshakeType.EncryptedExtensions, cancellationToken)
await ReceiveAndAppendAsync(
channel,
transcript,
DtlsHandshakeType.EncryptedExtensions,
serverPeer,
cancellationToken)
.ConfigureAwait(false);
DtlsHandshakeFrame certificateFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame certificateFrame = await ReceiveFrameAsync(
channel,
serverPeer,
cancellationToken)
.ConfigureAwait(false);
bool clientCertificateRequested = false;
if (certificateFrame.MessageType == DtlsHandshakeType.CertificateRequest)
{
DtlsHandshakeCodec.DecodeCertificateRequest(certificateFrame.Fragment);
transcript.Append(ToCompleteFrame(certificateFrame));
clientCertificateRequested = true;
certificateFrame = await ReceiveFrameAsync(channel, cancellationToken).ConfigureAwait(false);
certificateFrame = await ReceiveFrameAsync(
channel,
serverPeer,
cancellationToken).ConfigureAwait(false);
}

RequireMessage(certificateFrame, DtlsHandshakeType.Certificate);
Expand All @@ -214,7 +229,10 @@ await ReceiveAndAppendAsync(channel, transcript, DtlsHandshakeType.EncryptedExte
{
await ValidatePeerCertificateAsync(peerChain, cancellationToken).ConfigureAwait(false);
byte[] certificateVerifyTranscriptHash = transcript.GetHash();
DtlsHandshakeFrame certificateVerifyFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame certificateVerifyFrame = await ReceiveFrameAsync(
channel,
serverPeer,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(certificateVerifyFrame, DtlsHandshakeType.CertificateVerify);
DtlsCertificateAuthenticator.VerifyCertificateVerify(
Expand All @@ -226,7 +244,10 @@ await ReceiveAndAppendAsync(channel, transcript, DtlsHandshakeType.EncryptedExte
transcript.Append(ToCompleteFrame(certificateVerifyFrame));
}
byte[] finishedTranscriptHash = transcript.GetHash();
DtlsHandshakeFrame serverFinishedFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame serverFinishedFrame = await ReceiveFrameAsync(
channel,
serverPeer,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(serverFinishedFrame, DtlsHandshakeType.Finished);
byte[] expectedServerFinished = m_keyingContext.ComputeServerFinished(finishedTranscriptHash);
Expand Down Expand Up @@ -256,6 +277,7 @@ await ReceiveAndAppendAsync(channel, transcript, DtlsHandshakeType.EncryptedExte
await SendFlightAsync(channel, clientFinishedFrame, cancellationToken).ConfigureAwait(false);
CryptoUtils.ZeroMemory(clientFinished);
InstallApplicationKeys(isClient: true);
SetAuthenticatedPeer(channel, serverPeer);
}
finally
{
Expand All @@ -281,7 +303,7 @@ private async ValueTask AcceptAsync(IDtlsDatagramChannel channel, CancellationTo
RequireMessage(clientHelloFrame, DtlsHandshakeType.ClientHello);
clientHello = DtlsHandshakeCodec.DecodeClientHello(clientHelloFrame.Fragment);
ValidateClientHello(clientHello);
clientSource = source ?? GetCookieEndpoint(channel);
clientSource = source ?? GetPeerEndpoint(channel);
using var cookieProtector = new DtlsHelloRetryCookieProtector(cookieKey);
if (m_options.RequireHelloRetryRequestCookie &&
!cookieProtector.ValidateCookie(
Expand Down Expand Up @@ -361,11 +383,18 @@ await SendFlightAsync(channel, serverFinishedFrame, cancellationToken, clientSou
m_keyingContext.InstallApplicationSecrets(transcript.GetHash());
if (m_options.RequireClientCertificate)
{
await ReceiveClientAuthenticationAsync(channel, transcript, cancellationToken)
await ReceiveClientAuthenticationAsync(
channel,
transcript,
clientSource,
cancellationToken)
.ConfigureAwait(false);
}

DtlsHandshakeFrame clientFinishedFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame clientFinishedFrame = await ReceiveFrameAsync(
channel,
clientSource,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(clientFinishedFrame, DtlsHandshakeType.Finished);
byte[] expectedClientFinished = m_keyingContext.ComputeClientFinished(transcript.GetHash());
Expand All @@ -381,6 +410,7 @@ await ReceiveClientAuthenticationAsync(channel, transcript, cancellationToken)
}

InstallApplicationKeys(isClient: false);
SetAuthenticatedPeer(channel, clientSource);
}
finally
{
Expand Down Expand Up @@ -441,10 +471,21 @@ private static async ValueTask SendFlightAsync(

private static async ValueTask<DtlsHandshakeFrame> ReceiveFrameAsync(
IDtlsDatagramChannel channel,
IPEndPoint? expectedSource,
CancellationToken cancellationToken)
{
DtlsDatagram datagram = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
return DtlsHandshakeCodec.DecodeFrame(datagram.Payload.Span);
while (true)
{
DtlsDatagram datagram = await channel.ReceiveAsync(cancellationToken).ConfigureAwait(false);
IPEndPoint? source = datagram.Source ?? channel.RemoteEndpoint;
if (expectedSource is not null &&
(source is null || !expectedSource.Equals(source)))
{
continue;
}

return DtlsHandshakeCodec.DecodeFrame(datagram.Payload.Span);
}
}

private static async ValueTask<(DtlsHandshakeFrame Frame, IPEndPoint? Source)> ReceiveSourcedFrameAsync(
Expand Down Expand Up @@ -483,17 +524,24 @@ private async ValueTask SendClientAuthenticationAsync(
private async ValueTask ReceiveClientAuthenticationAsync(
IDtlsDatagramChannel channel,
DtlsTranscriptHash transcript,
IPEndPoint? expectedSource,
CancellationToken cancellationToken)
{
DtlsHandshakeFrame certificateFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame certificateFrame = await ReceiveFrameAsync(
channel,
expectedSource,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(certificateFrame, DtlsHandshakeType.Certificate);
transcript.Append(ToCompleteFrame(certificateFrame));
using CertificateCollection peerChain =
DtlsCertificateAuthenticator.DecodeCertificate(certificateFrame.Fragment);
await ValidatePeerCertificateAsync(peerChain, cancellationToken).ConfigureAwait(false);
byte[] certificateVerifyTranscriptHash = transcript.GetHash();
DtlsHandshakeFrame certificateVerifyFrame = await ReceiveFrameAsync(channel, cancellationToken)
DtlsHandshakeFrame certificateVerifyFrame = await ReceiveFrameAsync(
channel,
expectedSource,
cancellationToken)
.ConfigureAwait(false);
RequireMessage(certificateVerifyFrame, DtlsHandshakeType.CertificateVerify);
DtlsCertificateAuthenticator.VerifyCertificateVerify(
Expand Down Expand Up @@ -545,9 +593,13 @@ private static async ValueTask<DtlsHandshakeFrame> ReceiveAndAppendAsync(
IDtlsDatagramChannel channel,
DtlsTranscriptHash transcript,
DtlsHandshakeType messageType,
IPEndPoint? expectedSource,
CancellationToken cancellationToken)
{
DtlsHandshakeFrame frame = await ReceiveFrameAsync(channel, cancellationToken).ConfigureAwait(false);
DtlsHandshakeFrame frame = await ReceiveFrameAsync(
channel,
expectedSource,
cancellationToken).ConfigureAwait(false);
RequireMessage(frame, messageType);
if (messageType == DtlsHandshakeType.EncryptedExtensions)
{
Expand Down Expand Up @@ -705,10 +757,20 @@ private static HashAlgorithmName GetHashAlgorithm(DtlsCipherSuite cipherSuite)
: HashAlgorithmName.SHA256;
}

private IPEndPoint GetCookieEndpoint(IDtlsDatagramChannel channel)
private IPEndPoint GetPeerEndpoint(IDtlsDatagramChannel channel)
{
return channel.RemoteEndpoint ?? new IPEndPoint(m_endpoint.Address, m_endpoint.Port);
}

private static void SetAuthenticatedPeer(
IDtlsDatagramChannel channel,
IPEndPoint? peer)
{
if (peer is not null && channel is IDtlsAuthenticatedPeerChannel peerChannel)
{
peerChannel.SetAuthenticatedPeer(peer);
}
}
#endif

#if NET8_0_OR_GREATER
Expand Down
Loading
Loading