mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-17 13:21:25 +08:00
Count secured sends and describe Sign-only security accurately
Review feedback: - Secured NetworkMessages are encoded at the security boundary, outside UadpEncoder.EncodeAsync, which is where SentNetworkMessages and SentDataSetMessages are counted. Secured data messages were therefore never counted, and the discovery messages this branch now secures stopped being counted. EncodeAndWrapUadpAsync counts them the same way the encoder counts unsecured messages. - MessageSecurityMode.Sign signs without encrypting, so the DataSet layout of a Sign-only announcement stays readable. docs/PubSub.md, the MetaDataPublisher remarks, and SendMetaDataAnnouncementAsync now describe applying the configured SecurityMode, and limit confidentiality to SignAndEncrypt. Test: MetaDataPublisherTests.SecuredNetworkMessagesAreCountedAsSentAsync (the counter read 0 before the fix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+11
-8
@@ -285,9 +285,11 @@ The publisher-side `MetaDataPublisher` ([§6.2.2.5](https://reference.opcfoundat
|
||||
emits a retained `JsonMetaDataMessage` / `UadpDiscoveryResponseMessage`
|
||||
on the well-known `ua-metadata` topic at startup and after each
|
||||
configuration version bump; subscribers cache it before the first
|
||||
KeyFrame arrives. A connection configured for message security signs and
|
||||
encrypts the UADP announcement with the same keys as its DataSetMessages,
|
||||
so only subscribers that hold the keys can read the DataSet layout.
|
||||
KeyFrame arrives. A connection configured for message security applies its
|
||||
SecurityMode to the UADP announcement, with the same keys as its
|
||||
DataSetMessages: `Sign` lets subscribers verify the announcement, and
|
||||
`SignAndEncrypt` also hides the DataSet layout from applications that do not
|
||||
hold the keys.
|
||||
|
||||
A subscriber also registers the `DataSetMetaData` configured on each
|
||||
DataSetReader whose filter names an exact PublisherId, WriterGroupId, and
|
||||
@@ -1222,11 +1224,12 @@ subscribers that need to find publishers and bind to metadata at runtime.
|
||||
duplicate probes are suppressed, and identical responses are throttled.
|
||||
- MQTT publishes retained discovery messages on the standard status, connection,
|
||||
application, endpoint, and metadata topics.
|
||||
- A UADP connection configured for message security signs and encrypts its
|
||||
discovery requests, responses, and announcements with the keys of its
|
||||
SecurityGroup, so only applications that hold those keys can discover each
|
||||
other. The MQTT last-will status message is the exception: the broker
|
||||
publishes it later, so it is sent unsecured.
|
||||
- A UADP connection configured for message security applies its SecurityMode
|
||||
to its discovery requests, responses, and announcements, with the keys of
|
||||
its SecurityGroup: they are signed, and with `SignAndEncrypt` also
|
||||
encrypted. A secured connection therefore discovers only applications that
|
||||
hold the same keys. The MQTT last-will status message is the exception: the
|
||||
broker publishes it later, so it is sent unsecured.
|
||||
|
||||
```csharp
|
||||
PubSubDiscoveryResult result = await application.RequestDiscoveryAsync(
|
||||
|
||||
@@ -76,9 +76,10 @@ namespace Opc.Ua.PubSub.Application
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Announcements go through the connection's send pipeline, so a
|
||||
/// connection configured for message security signs and encrypts
|
||||
/// them with the same keys as its DataSetMessages, and refuses to
|
||||
/// send them when it cannot secure them.
|
||||
/// connection configured for message security applies its
|
||||
/// SecurityMode to them with the same keys as its DataSetMessages
|
||||
/// (signing, and with SignAndEncrypt also encryption), and refuses
|
||||
/// to send them when it cannot secure them.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Lifetime is owned by <see cref="PubSubApplication"/>: started
|
||||
@@ -532,7 +533,7 @@ namespace Opc.Ua.PubSub.Application
|
||||
writer.DataSetWriterId);
|
||||
|
||||
// The connection applies its message security, so a secured
|
||||
// connection never announces the DataSet layout in the clear.
|
||||
// connection never sends the announcement unsecured.
|
||||
await connection.SendMetaDataAnnouncementAsync(message, topic, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -2466,9 +2466,9 @@ namespace Opc.Ua.PubSub.Connections
|
||||
|
||||
/// <summary>
|
||||
/// Sends a DataSetMetaData announcement through the connection's send
|
||||
/// pipeline, so a connection configured for message security signs and
|
||||
/// encrypts the announcement like its DataSetMessages and never sends it
|
||||
/// in the clear.
|
||||
/// pipeline, so a connection configured for message security secures the
|
||||
/// announcement with its SecurityMode, like its DataSetMessages, and never
|
||||
/// sends it unsecured.
|
||||
/// </summary>
|
||||
/// <param name="announcement">UADP discovery response or JSON metadata message.</param>
|
||||
/// <param name="topic">Broker topic, or <see langword="null"/> for datagram transports.</param>
|
||||
@@ -2747,13 +2747,14 @@ namespace Opc.Ua.PubSub.Connections
|
||||
PubSubNetworkMessageContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ReadOnlyMemory<byte> wrapped;
|
||||
try
|
||||
{
|
||||
ReadOnlyMemory<byte> encoded = UadpEncoder.EncodeWithSecurityBoundary(
|
||||
message, context, out int payloadOffset);
|
||||
ReadOnlyMemory<byte> prefix = encoded[..payloadOffset];
|
||||
ReadOnlyMemory<byte> inner = encoded[payloadOffset..];
|
||||
return await m_securityWrapper!
|
||||
wrapped = await m_securityWrapper!
|
||||
.WrapAsync(prefix, inner, m_securityWrapOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@@ -2770,6 +2771,17 @@ namespace Opc.Ua.PubSub.Connections
|
||||
m_logger.UadpSecurityWrapFailed(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
// The encoder counts the messages it encodes; the secured encoding bypasses
|
||||
// it, so count the secured message in the same way.
|
||||
context.Diagnostics.Increment(PubSubDiagnosticsCounterKind.SentNetworkMessages);
|
||||
if (message is UadpNetworkMessage { DataSetMessages.Count: > 0 } data)
|
||||
{
|
||||
context.Diagnostics.Increment(
|
||||
PubSubDiagnosticsCounterKind.SentDataSetMessages,
|
||||
data.DataSetMessages.Count);
|
||||
}
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private INetworkMessageEncoder? ResolveEncoder()
|
||||
|
||||
@@ -225,6 +225,38 @@ namespace Opc.Ua.PubSub.Tests.Application
|
||||
Assert.That(response.DataSetMetaData?.Name, Is.EqualTo("pds-1"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A secured NetworkMessage is encoded at the security boundary, outside the
|
||||
/// encoder that counts the messages it encodes, and must still be counted as
|
||||
/// sent.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SecuredNetworkMessagesAreCountedAsSentAsync()
|
||||
{
|
||||
using PubSubSecurityKeyRing ring = NewKeyRing();
|
||||
var factory = new RecordingTransportFactory(UadpProfile, supportsTopics: false);
|
||||
await using IPubSubApplication app = BuildApp(
|
||||
UadpProfile,
|
||||
factory,
|
||||
securityKeyProvider: new StaticSecurityKeyProvider(SecurityGroupIdValue, ring));
|
||||
|
||||
await app.StartAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
await WaitUntilAsync(
|
||||
() => factory.Transport is { } t && t.Sends.Count >= 1,
|
||||
TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
||||
|
||||
// Each message is counted before it reaches the transport, so the counter
|
||||
// read after the sends is at least the number of sends.
|
||||
int sends;
|
||||
lock (factory.Transport!.Sends)
|
||||
{
|
||||
sends = factory.Transport.Sends.Count;
|
||||
}
|
||||
Assert.That(
|
||||
app.Diagnostics.Read(PubSubDiagnosticsCounterKind.SentNetworkMessages),
|
||||
Is.GreaterThanOrEqualTo(sends));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DisposeAsync_UnsubscribesFromRegistry()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user