Files
Marc SchierandCopilot 7f14fb864f Secure the UADP DataSetMetaData announcement on secured connections (#4712)
# Description

`MetaDataPublisher` encoded the DataSetMetaData announcement itself and
called `IPubSubTransport.SendAsync` directly. That bypassed the
connection's security wrapper and its fail-closed check. A WriterGroup
configured for `SignAndEncrypt` therefore announced its DataSet layout
(field names, types, version) unsigned and unencrypted. A secured
subscriber dropped the announcement (`Dropping unsecured inbound frame
... requiring SignAndEncrypt`, documented as an expected warning in
docs/PubSub.md), and any listener could read it. OPC 10000-14 §6.2.5.2
applies the WriterGroup's SecurityMode to the NetworkMessages it
publishes, and §7.2.4.6.3 carries the security of a discovery
announcement in its SecurityHeader.

Confirmed on the wire: the documented `udp-uadp` exchange sent secured
DataSetMessages plus one DiscoveryResponse (announcement type 2,
DataSetMetaData) with ExtendedFlags1 `0x81`, meaning the SecurityHeader
bit was clear.

Review feedback widened the scope to every UADP discovery message of a
secured connection, and to the diagnostics of secured messages.

Fix:

- `MetaDataPublisher` builds the announcement and hands it to the
connection (`PubSubConnection.SendMetaDataAnnouncementAsync`).
- One encode-and-secure step (`PubSubConnection.EncodeOutboundAsync`)
now serves data messages, discovery requests and responses, and the
announcements that UDP and Ethernet send to their transport-specific
discovery destination (ApplicationInformation, PublisherEndpoints,
PubSubConnection). On a connection configured for message security, it
applies the SecurityMode (`Sign`, or `SignAndEncrypt`) to UADP messages
and refuses to send any message it cannot secure.
- Before, a secured connection dropped its own discovery requests, so a
secured subscriber could not ask for metadata, writer configuration, or
endpoints. It also refused metadata and writer-configuration probe
responses, and sent the transport-specific announcements in plaintext.
- A discovery response or announcement that cannot be secured or sent is
logged (`DiscoveryMessageNotSent`, Warning) and dropped. Securing
announcements makes them depend on a current key; without this, a key
provider with no key yet, for example before a Security Key Service
delivers the first key, made `StartAsync` fail, and a probe response
that could not be secured ended the receive loop. Requests sent through
`RequestDiscoveryAsync` still throw to the caller.
- Secured messages are counted in `SentNetworkMessages` and
`SentDataSetMessages` like unsecured ones. Before, they bypassed the
encoder that counts them, so secured data messages were never counted.
- `UadpDiscoveryCoder` can encode a discovery message with the
SecurityHeader bit set, and reports the security boundary after the
PublisherId (and DataSetClassId), which is where
`UadpDecoder.TryReadOuterPrefix` ends the prefix.
`UadpEncoder.EncodeWithSecurityBoundary` accepts discovery messages.
- The receive path is unchanged: it already unwraps secured discovery
frames and registers authenticated metadata.
- Not changed: the MQTT last-will status message is registered with the
broker when the connection opens and published later, so it cannot carry
a current SecurityHeader; it stays unsecured.
- docs/PubSub.md no longer lists the unsecured-frame warning. It
describes how the SecurityMode applies to announcements and discovery
(`Sign` authenticates; `SignAndEncrypt` also encrypts) and the last-will
exception.

Tests:

-
`MetaDataPublisherTests.UadpPathSecuresTheAnnouncementOfASecuredWriterGroupAsync`:
the announcement of a SignAndEncrypt WriterGroup carries a
SecurityHeader, does not contain the DataSet name in cleartext, and
unwraps and decodes with the subscriber's keys. Before the fix, the
announcement had no SecurityHeader.
- `MetaDataPublisherTests.SecuredNetworkMessagesAreCountedAsSentAsync`:
before the fix, `SentNetworkMessages` read 0 on a secured connection.
-
`PubSubDiscoveryTests.SecuredSubscriberDiscoversSecuredPublisherWithoutPlaintextFramesAsync`:
a secured subscriber and a secured publisher, connected through an
in-memory transport that also sends announcements to a separate
destination, discover metadata, writer configuration, and endpoints, and
no frame lacks a SecurityHeader. Before the fix, the metadata request
was dropped. With only the announcement change reverted, the
PublisherEndpoints response was dropped.
-
`PubSubDiscoveryTests.StartupAnnouncementsWithoutACurrentKeyDoNotFailTheStartAsync`
and `ReceiveLoopSurvivesADiscoveryResponseThatCannotBeSecuredAsync`:
both failed before the fail-soft change.
-
`UadpDiscoveryTests.SecuredDiscoveryMessagesSplitWhereTheReceiverExpects`:
for requests and responses, with and without a DataSetClassId, the
secured encoding differs from the unsecured one only by the
SecurityHeader flag, and its boundary equals the receiver's prefix
length.
-
`UadpDiscoveryTests.EncodeWithSecurityBoundaryRejectsAMessageItCannotSecure`.

### Validation

| Command | Result |
| --- | --- |
| `dotnet test tests/Opc.Ua.PubSub.Tests -f net10.0
-p:CustomTestTarget=net10.0` | 1498 passed, 1 skipped (the UDP multicast
loopback discovery test, which this host skips) |
| `dotnet test tests/Opc.Ua.PubSub.Tests -f net48
-p:CustomTestTarget=net48` | 1498 passed, 1 skipped |
| `dotnet test` on net10.0 for `Opc.Ua.PubSub.Udp.Tests`, `.Eth.Tests`,
`.Mqtt.Tests`, `.Server.Tests`, `fuzzing/Opc.Ua.PubSub.Fuzz.Tests` |
234, 55, 157, 177, 149 passed |
| `dotnet test` on net10.0 for `Opc.Ua.PubSub.Kafka.Tests`,
`.Adapter.Tests`, `.Diagnostics.Tests`, `Opc.Ua.Redundancy.PubSub.Tests`
(on 9417ccbbb) | 92 (1 skipped), 152, 53 (1 skipped), 45 passed |
| `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe
--treenode-filter "/*/*/PubSubAotTests/*"` (JIT; the AOT job failed on
94d43919a) | 7 of 7 passed, including `StartsAndStopsPublisher_UdpUadp`
|
| `dotnet build src/Opc.Ua.PubSub -c Release` (all target frameworks) |
0 warnings |
| The docs/PubSub.md first exchange, run verbatim, with #4710 applied
(before the review changes) | The announcement is secured on the wire
(ExtendedFlags1 `0x91`, 268 bytes); 14 DataSets; no unsecured-frame
warning; the subscriber logs `Registered inbound metadata for writer 1`
|
| After merging master (#4710, #4711, #4716), on 83567cecf: `dotnet
build src/Opc.Ua.PubSub -c Release` | 0 warnings |
| Same merge: `dotnet test tests/Opc.Ua.PubSub.Tests -f net10.0`; the
changed fixtures on net48; `dotnet test tests/Opc.Ua.PubSub.Udp.Tests -f
net10.0` | 1498 passed, 1 skipped; 132 passed, 1 skipped; 237 passed |
| Same merge: `Opc.Ua.Aot.Tests.exe --treenode-filter
"/*/*/PubSubAotTests/*"` (JIT) | 8 of 8 passed, including #4710's
`SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync` |
| Same merge: the docs/PubSub.md first exchange, run verbatim on
`opc.udp://127.0.0.1:4840`, then the publisher alone against a UDP
listener | 9 DataSets in 10 seconds and no warning on either side; the
listener saw 9 DataSetMessages and 1 DiscoveryResponse (the startup
announcement), all with the SecurityHeader bit set |

## Related Issues

- No tracking issue. Found while verifying the PubSub first-exchange
documentation (follow-up to #4555).

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-09 16:52:45 +02:00
..