Withhold ECC policies under FipsOnly on .NET Framework

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>
This commit is contained in:
Roman Ettlinger
2026-10-08 15:20:09 +02:00
parent 97894b326e
commit a2ecf13b19
6 changed files with 61 additions and 6 deletions
@@ -66,7 +66,8 @@ SecureChannel and user-token policies (`ECC_nistP256`, `ECC_nistP384`,
`ECC_brainpoolP256r1`, `ECC_brainpoolP384r1`) are available there.
**Migration:** nothing is required. Note that on .NET Framework the agreement
step does not run in a platform-validated cryptographic module; deployments
step does not run in a platform-validated cryptographic module, so
`CryptoCompliancePolicy.FipsOnly` withholds the ECC policies there; deployments
with FIPS or certified-module requirements should use the .NET 8+ build or an
RSA policy. The `_AesGcm` and `_ChaChaPoly` ECC variants still need .NET 8 or
later, and `SecurityPolicies.GetInfo` returns `null` for them on .NET Framework.
+3 -1
View File
@@ -253,7 +253,9 @@ These algorithms are **not** FIPS-approved and are enabled by default:
| SHA-1 and P-SHA1 (Basic128Rsa15, Basic256) | Deprecated for new signatures by SP 800-131A |
`net48` additionally uses the `BouncyCastle.Cryptography` package for elliptic curve
certificate building. That package is **not** validated — the validated Bouncy Castle product is a
certificate building and for the raw ECDH agreement of the ECC security policies. Because that
agreement bypasses the provider registry, `FipsOnly` withholds every ECC policy on `net48`, including
the NIST curves. That package is **not** validated — the validated Bouncy Castle product is a
separate, commercially licensed distribution — so **that target framework cannot make a FIPS claim at
all**.
+2 -1
View File
@@ -308,7 +308,8 @@ which `Opc.Ua.Security.Certificates` already references on that target.
The ephemeral keys, ECDSA signatures and HKDF stay on the platform (CNG)
providers; only the agreement step is managed code. Deployments bound to
FIPS-validated or other certified cryptographic modules should note that
this step is not performed by a validated module on .NET Framework; use the
this step is not performed by a validated module on .NET Framework, so
`CryptoCompliancePolicy.FipsOnly` withholds the ECC policies there; use the
.NET 8+ build, or an RSA policy, where that matters. The .NET 8+ builds use
`ECDiffieHellman.DeriveRawSecretAgreement` and do not load BouncyCastle for it.
+2 -1
View File
@@ -66,7 +66,8 @@ SecureChannel and user-token policies (`ECC_nistP256`, `ECC_nistP384`,
`ECC_brainpoolP256r1`, `ECC_brainpoolP384r1`) are available there.
**Migration:** nothing is required. Note that on .NET Framework the agreement
step does not run in a platform-validated cryptographic module; deployments
step does not run in a platform-validated cryptographic module, so
`CryptoCompliancePolicy.FipsOnly` withholds the ECC policies there; deployments
with FIPS or certified-module requirements should use the .NET 8+ build or an
RSA policy. The `_AesGcm` and `_ChaChaPoly` ECC variants still need .NET 8 or
later, and `SecurityPolicies.GetInfo` returns `null` for them on .NET Framework.
@@ -77,7 +77,27 @@ namespace Opc.Ua
SecurityPolicyInfo? info = SecurityPolicies.Default.GetInfoIgnoringPlatformSupport(
securityPolicyUri ?? string.Empty);
return info != null && info.IsFipsApproved;
return info != null && info.IsFipsApproved && !UsesUnvalidatedKeyAgreement(info);
}
/// <summary>
/// Whether this build performs a step of the policy outside any
/// validated module, whatever providers are registered.
/// </summary>
/// <remarks>
/// The .NET Framework BCL cannot return the raw ECDH secret OPC UA
/// requires, so that build computes the agreement in the managed
/// BouncyCastle implementation. It bypasses the provider registry, so
/// the auditor cannot see it, and the ECC policies are withheld instead.
/// </remarks>
private static bool UsesUnvalidatedKeyAgreement(SecurityPolicyInfo info)
{
#if NETFRAMEWORK
return info.CertificateKeyFamily == CertificateKeyFamily.ECC;
#else
_ = info;
return false;
#endif
}
/// <summary>
@@ -280,10 +280,12 @@ namespace Opc.Ua.Core.Tests.Security.Crypto
[TestCase(SecurityPolicies.Basic256Sha256)]
[TestCase(SecurityPolicies.Aes128_Sha256_RsaOaep)]
[TestCase(SecurityPolicies.Aes256_Sha256_RsaPss)]
[TestCase(SecurityPolicies.RSA_DH_AesGcm)]
#if !NETFRAMEWORK
[TestCase(SecurityPolicies.ECC_nistP256)]
[TestCase(SecurityPolicies.ECC_nistP384)]
[TestCase(SecurityPolicies.ECC_nistP256_AesGcm)]
[TestCase(SecurityPolicies.RSA_DH_AesGcm)]
#endif
public void FipsOnlyPermitsApprovedPolicies(string securityPolicyUri)
{
Assert.That(
@@ -292,6 +294,28 @@ namespace Opc.Ua.Core.Tests.Security.Crypto
Is.True);
}
#if NETFRAMEWORK
/// <summary>
/// On .NET Framework the ECDH agreement runs in BouncyCastle, outside
/// any validated module, so even the NIST ECC policies are withheld.
/// </summary>
[Test]
[TestCase(SecurityPolicies.ECC_nistP256)]
[TestCase(SecurityPolicies.ECC_nistP384)]
[TestCase(SecurityPolicies.ECC_nistP256_AesGcm)]
[TestCase(SecurityPolicies.ECC_nistP384_AesGcm)]
public void FipsOnlyWithholdsEccPoliciesOnNetFramework(string securityPolicyUri)
{
SecurityPolicyInfo? info = SecurityPolicies.Default.GetInfoIgnoringPlatformSupport(
securityPolicyUri);
Assert.That(info?.IsFipsApproved, Is.True);
Assert.That(
CryptoCompliance.IsPolicyPermitted(
securityPolicyUri, CryptoCompliancePolicy.FipsOnly),
Is.False);
}
#endif
[Test]
[TestCase(SecurityPolicies.Basic128Rsa15)]
[TestCase(SecurityPolicies.Basic256)]
@@ -324,12 +348,18 @@ namespace Opc.Ua.Core.Tests.Security.Crypto
ArrayOf<string> filtered = CryptoCompliance.FilterPolicies(
policies, CryptoCompliancePolicy.FipsOnly);
#if NETFRAMEWORK
// The ECDH agreement is not validated on .NET Framework.
Assert.That(filtered.Count, Is.EqualTo(1));
Assert.That(filtered[0], Is.EqualTo(SecurityPolicies.Basic256Sha256));
#else
Assert.That(filtered.Count, Is.EqualTo(2));
Assert.Multiple(() =>
{
Assert.That(filtered[0], Is.EqualTo(SecurityPolicies.Basic256Sha256));
Assert.That(filtered[1], Is.EqualTo(SecurityPolicies.ECC_nistP256));
});
#endif
}
/// <summary>