Skip to content

Enable AES-GCM and ChaCha20-Poly1305 policies on net48 via BouncyCastle (stacked on #4703) - #4705

Merged
romanett merged 11 commits into
masterfrom
romanett/net48-aead-polyfill
Oct 9, 2026
Merged

romanett merged 11 commits into
masterfrom
romanett/net48-aead-polyfill

Conversation

@romanett

@romanett romanett commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #4703 (now merged to master; this PR has been retargeted to master).

Part of #4510. #4703 enabled the CBC ECC policies on net48 through a BouncyCastle raw-ECDH polyfill. The AEAD policies were still unavailable there because .NET Framework has no AesGcm or ChaCha20Poly1305. This PR adds those ciphers the same way, so on net48 all of these are now offered:
ECC_{nistP256,nistP384,brainpoolP256r1,brainpoolP384r1}_AesGcm / _ChaChaPoly, RSA_DH_AesGcm, RSA_DH_ChaChaPoly.

Changes

  • Polyfill src/Opc.Ua.Security.Certificates/Org.BouncyCastle/AeadCiphers.cs (internal, #if NETFRAMEWORK, next to EcdhRawAgreement): AesGcm and ChaCha20Poly1305 types with the .NET 8 BCL surface. They wrap BouncyCastle's GcmBlockCipher(AesEngine) and Org.BouncyCastle.Crypto.Modes.ChaCha20Poly1305.
    • A tag mismatch throws CryptographicException, never BC's InvalidCipherTextException, so PlatformSymmetricCryptoProvider.DecryptAuthenticated still returns false and the channel still raises a protocol error.
    • GCM releases plaintext blocks before it checks the tag, so plaintext stays in a scratch buffer and is copied out only after the tag verifies. On failure the destination is cleared, as the BCL does. Scratch buffers and the key copy are zeroed.
  • Opc.Ua.Core: aliases the polyfill types on NETFRAMEWORK and removes the NET8_0_OR_GREATER gates in SecurityPolicies (SupportsAesGcmPolicy / SupportsChaCha20Poly1305Policy), CryptoUtils (SymmetricEncryptAndSign / SymmetricDecryptAndVerify, AEAD helpers) and PlatformSymmetricCryptoProvider. The channel, the provider facet and the encrypted ECC user tokens (EncryptedSecret, through CryptoUtils) now run the same code on every target. .NET 8+ keeps the BCL ciphers.
  • FIPS (requested by the Enable ECC security policies on net48 via a BouncyCastle raw-ECDH polyfill #4703 session, after a2ecf13): under CryptoCompliancePolicy.FipsOnly, CryptoCompliance.IsPolicyPermitted now also withholds every AEAD policy on .NET Framework. That adds RSA_DH_AesGcm, which is FIPS-approved and not ECC. The BouncyCastle cipher runs outside any validated module and the user-token path bypasses the provider registry, so the auditor cannot see it. Tests and docs/CryptoProvider.md are updated to match.
  • Docs: EccProfiles (Known Limitations), migration certificates.md (and the bundled skill copy), ProductionChecklist, Profiles, WhatsNewIn2.0, CryptoProvider. They state that on net48 these primitives run in managed BouncyCastle, which is not a validated (FIPS) module and not hardware-accelerated or hardened against cache-timing attacks like CNG AES.

Tests

  • New AeadPolyfillTests (Core.Tests):
    • Known answers: NIST GCM test cases 2/4 (AES-128) and 14/16 (AES-256), plus RFC 8439 §2.8.2 for ChaCha20-Poly1305. They run through PlatformSymmetricCryptoProvider on every TFM and through the polyfill directly.
    • Tampering with the tag, the ciphertext or the AAD gives false from the provider, and CryptographicException with a cleared plaintext from the polyfill.
    • Argument validation.
    • BCL cross-check: on .NET 8+, Core.Tests also compiles AeadCiphers.cs (AEAD_POLYFILL_CROSSCHECK, test-only BouncyCastle.Cryptography reference). Polyfill and BCL must produce identical ciphertext and tag, and each must decrypt the other's output: 3 algorithms × 8 message lengths (0 to 8192) × 3 AAD lengths.
  • CryptoProviderRegistryTests: RSA_DH_AesGcm is permitted under FipsOnly only off .NET Framework and withheld on it.
  • Tests that Assert.Ignore when AEAD is unavailable now run on net48, e.g. EccSecretRejectsDifferentNegotiatedPolicyAsync and the RSA_DH_AesGcm server session tests.

Local results

Run Result
Core.Tests net48, full (before the FIPS commit) 6049 passed, 0 failed, 88 skipped
Core.Tests crypto/AEAD/ECC/EncryptedSecret/Nonce subset, net48 / net10.0 278 / 343 passed, 0 skipped
Server.Tests session-security/ECC subset, net48 128 passed, 0 skipped
EccInteropTests net48 vs open62541 1.5.8 (OpenSSL) peer all 16 AEAD cases pass, 4 of them two-renewal token-renewal runs (ECC_nistP256_AesGcm/_ChaChaPoly × Sign/SignAndEncrypt × Anonymous/UserName, both directions). Remaining 8 failures are the documented open62541 CBC Sign-only HKDF differences in expected-differences.json
EccInteropTests net48 vs 1.5.378 LegacyPeer unchanged 26 passed / 28 skipped

Note on the acceptance criterion: the 1.5.378 LegacyPeer does not implement the AEAD policies, so its _AesGcm/_ChaChaPoly/*AeadTokenRenewalAsync cases skip on every TFM ("The peer (UA-.NETStandard) does not declare …"). Before this PR they skipped on the 2.0 side; now they get past it and stop at the peer check. The open62541 peer is the real cross-stack check of the net48 AEAD path against a non-BouncyCastle implementation.

PubSub DTLS on net48

The DTLS 1.3 transport (src/Opc.Ua.PubSub.Udp/Dtls) was compiled out on .NET Framework, so net48 registered no DTLS profiles. It now reuses the same polyfills (Opc.Ua.Security.Certificates gains InternalsVisibleTo Opc.Ua.PubSub.Udp):

  • The NET8_0_OR_GREATER gates in the handshake, ECDHE, record protection and profile probe are removed.
  • Two .NET 8 APIs get net48 fallbacks: the AES-ECB record-number mask uses CreateEncryptor, and the handshake random uses RandomNumberGenerator.Create.
  • The HKDF probe now checks the managed DtlsHkdf that the key schedule actually uses.
  • The public IsAeadSupported / IsRawSharedSecretSupported probes are kept and now return true everywhere.
  • The DTLS tests now also run on net48. The net48 "registers no profiles" test now asserts that the NIST AEAD profiles are registered. The socket transport-peer tests and the benchmarks stay .NET 8-only because they use .NET 8 socket APIs.
  • docs/PubSub.md DTLS profile table: the net48 column is updated.

Results: PubSub.Udp.Tests net48, full suite: 229 passed, 0 skipped. It includes complete NIST AEAD, brainpool and integrity-only handshakes and ChaCha20 records. The DTLS subset is 76 on net48 vs 81 on net10.0; the 5 still-gated socket-peer tests are the difference.

Note: DTLS profile selection does not consult CryptoCompliancePolicy on any target. That is pre-existing and unchanged here.

🤖 Generated with Claude Code

romanett and others added 9 commits October 8, 2026 15:08
…yfill (#4510)

OPC UA Part 6 feeds the raw ECDH shared secret into HKDF, which the .NET
Framework BCL cannot produce (DeriveKeyMaterial always hashes it), so the
ECC SecureChannel and user-token policies were unavailable on net48.

- Add an internal DeriveRawSecretAgreement extension on ECDiffieHellman in
  Opc.Ua.Security.Certificates (net48 only) that computes the agreement with
  BouncyCastle's ECDHBasicAgreement. Keys, ECDSA and HKDF stay on CNG; the
  extension mirrors the .NET 8 signature so Nonce compiles unchanged.
- Drop SupportsRawEccSecretAgreement; the CBC ECC policies (nistP256/384,
  brainpoolP256r1/384r1) are now offered on every target. The AES-GCM and
  ChaCha20-Poly1305 variants still need .NET 8.
- Tests: symmetric agreement on all four curves, a net48 oracle against
  CNG's hashed agreement, curve-mismatch and off-curve rejection; tests that
  asserted the old limitation updated.
- Docs: EccProfiles, migration guide, checklist, profiles, what's new; note
  the agreement step is not a validated module on net48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The net48 raw ECDH agreement runs in BouncyCastle and bypasses the crypto
provider registry, so the auditor could not see it and FipsOnly still
permitted the NIST ECC policies. CryptoCompliance.IsPolicyPermitted now
withholds every ECC policy on .NET Framework under FipsOnly. Docs and the
bundled migration skill copy updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le (#4510)

.NET Framework has no AesGcm or ChaCha20Poly1305, so the AEAD security
policies (ECC_*_AesGcm, ECC_*_ChaChaPoly, RSA_DH_AesGcm, RSA_DH_ChaChaPoly)
were unavailable on net48 even after the raw-ECDH polyfill.

- Add internal AesGcm and ChaCha20Poly1305 types next to EcdhRawAgreement
  in Opc.Ua.Security.Certificates (net48 only). They mirror the .NET 8 BCL
  surface and run BouncyCastle's GcmBlockCipher over AesEngine and its
  ChaCha20Poly1305. A tag mismatch throws CryptographicException (not
  InvalidCipherTextException) and clears the plaintext; nothing unverified
  is released and scratch buffers and the key copy are zeroed.
- Opc.Ua.Core aliases them on NETFRAMEWORK and drops the NET8_0_OR_GREATER
  gates in SecurityPolicies, CryptoUtils and PlatformSymmetricCryptoProvider,
  so the channel, the provider facet and the encrypted ECC user tokens use
  the same code on every target. .NET 8+ keeps the BCL ciphers.
- Tests: NIST GCM (AES-128/256) and RFC 8439 2.8.2 known answers through the
  platform provider and the polyfill, tag/ciphertext/AAD tamper rejection.
  Core.Tests also compiles the polyfill on .NET 8+ to cross-check it against
  the BCL on random inputs.
- Docs: EccProfiles, migration guide, checklist, profiles, what's new.

PubSub DTLS record protection is still .NET 8 only (follow-up).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…147' into romanett/net48-aead-polyfill

# Conflicts:
#	docs/EccProfiles.md
#	docs/migrate/2.0.x/certificates.md
The net48 AES-GCM and ChaCha20-Poly1305 now run in BouncyCastle, and the
encrypted user tokens reach them without the provider registry, so the
auditor cannot see them. Extend the FipsOnly check that already withholds
the ECC policies on .NET Framework to every AEAD policy, which adds
RSA_DH_AesGcm (FIPS-approved and not ECC). Tests and CryptoProvider.md
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ills

The DTLS 1.3 transport was compiled out on .NET Framework (no raw ECDH
agreement, no AesGcm/ChaCha20Poly1305), so net48 registered no DTLS
profiles. Reuse the polyfills from Opc.Ua.Security.Certificates (now
InternalsVisibleTo Opc.Ua.PubSub.Udp) and drop the NET8_0_OR_GREATER gates
in the handshake, ECDHE key exchange, record protection and profile probe.

- net48 fallbacks for the two remaining .NET 8 APIs: AES-ECB record-number
  mask via CreateEncryptor, RandomNumberGenerator.Create for the handshake
  random. The HKDF probe now exercises the managed DtlsHkdf the key
  schedule actually uses instead of the BCL HKDF.
- The IsAeadSupported / IsRawSharedSecretSupported probes stay public and
  now return true on every target.
- Tests: the DTLS handshake, keying, ECDHE, record protection and
  authenticator tests now also run on net48 (net6+ APIs replaced); the
  net48 "registers no profiles" test becomes one asserting the NIST AEAD
  profiles. The socket transport-peer tests and the benchmarks stay .NET 8.
- docs/PubSub.md: net48 column of the DTLS profile table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UpdateIdentityAsync(provider, overrideUri) passes only the override as the
enabled policy, but the selector still accepts token policies with an empty
SecurityPolicyUri because they inherit the channel policy. When the override
itself cannot be satisfied (an ECC policy whose curve the instance
certificate does not match, or an ECC policy on an RSA instance
certificate), the selection silently fell back to the inherited policy: the
session logged the switch, fetched an EphemeralKey for the override, and
then encrypted the token under the channel policy against it. With a
different curve that failed with a raw CryptographicException/ArgumentException.

net48 exposed it: ECC_nistP256_AesGcm is unavailable there, so
FailedEccToEccOverrideKeepsSessionReconnectable overrode P-256 -> P-384.

The override is now verified against the selected policy's effective
SecurityPolicyUri and refused with BadIdentityTokenRejected (as documented)
before any key is fetched or state committed. Adds a cross-curve test and
lets UpdateIdentityAsyncSwitchesToEccUserTokenPolicy accept that refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The newly ungated registry test assumes optional runtime primitives are universally available, causing valid platforms to fail.

3 open findings
What changed in this PR

Extends #4703 by enabling AEAD security policies and DTLS 1.3 on .NET Framework 4.8 through BouncyCastle polyfills.

Changes:

  • Adds AES-GCM and ChaCha20-Poly1305 polyfills with known-answer and interoperability tests.
  • Enables AEAD SecureChannel, encrypted-token, and DTLS paths on net48.
  • Updates FIPS filtering and platform-support documentation.
File Description
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsRecordProtectionTests.cs Enables record-protection tests on net48.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsProfileRegistryTests.cs Updates profile-registration expectations.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsKeyScheduleTests.cs Enables key-schedule tests on net48.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsHandshakeKeyingContextTests.cs Uses cross-target hashing and randomness.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsHandshakeContextTests.cs Enables handshake tests on net48.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsEcdheKeyExchangeTests.cs Enables ECDHE tests on net48.
tests/​Opc.Ua.PubSub.Udp.Tests/​Dtls/​DtlsCertificateAuthenticatorTests.cs Enables certificate-authentication tests.
tests/​Opc.Ua.Core.Tests/​Security/​Crypto/​CryptoProviderRegistryTests.cs Verifies net48 FIPS exclusions.
tests/​Opc.Ua.Core.Tests/​Security/​Crypto/​AeadPolyfillTests.cs Adds AEAD vectors, tampering, and cross-check tests.
tests/​Opc.Ua.Core.Tests/​Opc.Ua.Core.Tests.csproj Compiles the polyfill for BCL cross-checking.
src/​Opc.Ua.Security.Certificates/​Org.BouncyCastle/​AeadCiphers.cs Implements BouncyCastle AEAD polyfills.
src/​Opc.Ua.Security.Certificates/​Opc.Ua.Security.Certificates.csproj Grants PubSub access to internal polyfills.
src/​Opc.Ua.PubSub.Udp/​Dtls/​DtlsRecordProtection.cs Enables AEAD records and AES masking on net48.
src/​Opc.Ua.PubSub.Udp/​Dtls/​DtlsProfileRegistry.cs Probes managed DTLS primitives.
src/​Opc.Ua.PubSub.Udp/​Dtls/​DtlsHandshakeContext.cs Enables DTLS handshakes on net48.
src/​Opc.Ua.PubSub.Udp/​Dtls/​DtlsEcdheKeyExchange.cs Uses the raw-ECDH polyfill.
src/​Opc.Ua.Core/​Security/​SecurityPolicies.cs Exposes AEAD policies on net48.
src/​Opc.Ua.Core/​Security/​Crypto/​PlatformSymmetricCryptoProvider.cs Routes AEAD operations through polyfills.
src/​Opc.Ua.Core/​Security/​Crypto/​CryptoCompliance.cs Withholds managed AEAD under FIPS-only mode.
src/​Opc.Ua.Core/​Security/​Certificates/​CryptoUtils.cs Enables AEAD channel and token processing.
docs/​WhatsNewIn2.0.md Announces cross-target AEAD availability.
docs/​PubSub.md Documents net48 DTLS support.
docs/​Profiles.md Updates ECC platform requirements.
docs/​ProductionChecklist.md Adds managed-crypto deployment guidance.
docs/​migrate/​2.0.x/​certificates.md Updates migration guidance.
docs/​EccProfiles.md Documents AEAD limitations and risks.
docs/​CryptoProvider.md Documents FIPS implications.
.agents/​skills/​opcua-v20-migration/​references/​stack-migration/​certificates.md Synchronizes bundled migration guidance.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread tests/Opc.Ua.PubSub.Udp.Tests/Dtls/DtlsProfileRegistryTests.cs Outdated
Comment thread src/Opc.Ua.PubSub.Udp/Dtls/DtlsProfileRegistry.cs
Comment thread tests/Opc.Ua.Core.Tests/Security/Crypto/AeadPolyfillTests.cs Outdated
…, sealed fixture

- DtlsProfileRegistryTests: expect each NIST AEAD profile exactly when the
  probed primitives allow it; only on .NET Framework, where the BouncyCastle
  ciphers, managed HKDF and CNG NIST curves always exist, assert they do.
- DtlsPrimitiveSupport / Probe docs describe the BouncyCastle and managed
  HKDF fallbacks instead of "BCL APIs only".
- Seal AeadPolyfillTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from romanett/ua-netstandard-4510-08b147 to master October 8, 2026 19:52
@marcschier

Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

…-polyfill

# Conflicts:
#	.agents/skills/opcua-v20-migration/references/stack-migration/certificates.md
#	docs/CryptoProvider.md
#	docs/EccProfiles.md
#	docs/ProductionChecklist.md
#	docs/Profiles.md
#	docs/WhatsNewIn2.0.md
#	docs/migrate/2.0.x/certificates.md
#	src/Opc.Ua.Core/Security/Crypto/CryptoCompliance.cs
#	tests/Opc.Ua.Core.Tests/Security/Crypto/CryptoProviderRegistryTests.cs

Copilot AI commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged master and resolved the conflicts, preserving the AEAD and FIPS changes. Merge commit: 2754672d.

Copilot AI requested a review from marcschier October 8, 2026 20:05
@romanett
romanett enabled auto-merge (squash) October 8, 2026 20:12
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 88.58% (305981/345410 lines) >= 70.00%
✅ Project branch rate 79.02% >= 60.00%
✅ Patch coverage 100.00% (11/11 changed lines) >= 60.00% (<= 100 changed lines, advisory)
ℹ️ Baseline delta (advisory) +14.98 pp 73.60% recorded

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 87.72%, branch 78.23%.

@codecov

codecov Bot commented Oct 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.82%. Comparing base (1da9fc0) to head (2754672).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4705      +/-   ##
==========================================
- Coverage   82.82%   82.82%   -0.01%     
==========================================
  Files        2264     2264              
  Lines      345410   345410              
  Branches    60182    60182              
==========================================
- Hits       286081   286074       -7     
- Misses      39259    39262       +3     
- Partials    20070    20074       +4     
Flag Coverage Δ
actions 82.82% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...c/Opc.Ua.Core/Security/Certificates/CryptoUtils.cs 72.27% <ø> (ø)
...rc/Opc.Ua.Core/Security/Crypto/CryptoCompliance.cs 88.15% <100.00%> (ø)
...Security/Crypto/PlatformSymmetricCryptoProvider.cs 89.47% <ø> (+1.50%) ⬆️
src/Opc.Ua.Core/Security/SecurityPolicies.cs 85.76% <ø> (ø)
src/Opc.Ua.PubSub.Udp/Dtls/DtlsEcdheKeyExchange.cs 79.71% <ø> (ø)
src/Opc.Ua.PubSub.Udp/Dtls/DtlsHandshakeContext.cs 90.47% <ø> (ø)
src/Opc.Ua.PubSub.Udp/Dtls/DtlsProfileRegistry.cs 80.80% <100.00%> (-3.46%) ⬇️
src/Opc.Ua.PubSub.Udp/Dtls/DtlsRecordProtection.cs 87.15% <100.00%> (+0.08%) ⬆️

... and 29 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants