Auto: opcuaclient-5 — CRL/revocation handling
Adds explicit revoked-vs-untrusted distinction to the OpcUaClient driver's
server-cert validation hook, plus three new knobs on a new
OpcUaCertificateValidationOptions sub-record:
RejectSHA1SignedCertificates (default true — SHA-1 is OPC UA spec-deprecated;
this is a deliberately tighter default)
RejectUnknownRevocationStatus (default false — keeps brownfield deployments
without CRL infrastructure working)
MinimumCertificateKeySize (default 2048)
The validator hook now runs whether or not AutoAcceptCertificates is set:
revoked / issuer-revoked certs are always rejected with a distinct
"REVOKED" log line; SHA-1 + small-key certs are rejected per policy;
unknown-revocation gates on the new flag; untrusted still honours
AutoAccept.
Decision pipeline factored into a static EvaluateCertificateValidation
helper with a CertificateValidationDecision record so unit tests cover
all branches without needing to spin up an SDK CertificateValidator.
CRL files themselves: the OPC UA SDK reads them automatically from the
crl/ subdir of each cert store — no driver-side wiring needed.
Documented on the new options record.
Tests (12 new) cover defaults, every branch of the decision pipeline,
SHA-1 detection (custom X509SignatureGenerator since .NET 10's
CreateSelfSigned refuses SHA-1), and key-size detection. All 127
OpcUaClient unit tests still pass.
Closes #277
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Opc.Ua.Configuration;
|
||||
@@ -248,17 +249,11 @@ public sealed class OpcUaClientDriver(OpcUaClientDriverOptions options, string d
|
||||
|
||||
await config.ValidateAsync(ApplicationType.Client, ct).ConfigureAwait(false);
|
||||
|
||||
// Attach a cert-validator handler that honours the AutoAccept flag. Without this,
|
||||
// AutoAcceptUntrustedCertificates on the config alone isn't always enough in newer
|
||||
// SDK versions — the validator raises an event the app has to handle.
|
||||
if (_options.AutoAcceptCertificates)
|
||||
{
|
||||
config.CertificateValidator.CertificateValidation += (s, e) =>
|
||||
{
|
||||
if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
|
||||
e.Accept = true;
|
||||
};
|
||||
}
|
||||
// Attach a cert-validator handler. The SDK's AutoAcceptUntrustedCertificates flag
|
||||
// alone isn't always enough in newer SDK versions — the validator raises an event
|
||||
// the app has to handle. We also use this hook to enforce the
|
||||
// CertificateValidation policy (revoked, SHA-1, key size) regardless of AutoAccept.
|
||||
config.CertificateValidator.CertificateValidation += OnCertificateValidation;
|
||||
|
||||
// Ensure an application certificate exists. The SDK auto-generates one if missing.
|
||||
app.ApplicationConfiguration = config;
|
||||
@@ -268,6 +263,128 @@ public sealed class OpcUaClientDriver(OpcUaClientDriverOptions options, string d
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cert-validator callback. Funnels into <see cref="EvaluateCertificateValidation"/>
|
||||
/// for testability — the static helper takes the cert + status code + options and
|
||||
/// returns the decision, which this method then applies to the SDK's event args.
|
||||
/// </summary>
|
||||
private void OnCertificateValidation(object sender, Opc.Ua.CertificateValidationEventArgs e)
|
||||
{
|
||||
var decision = EvaluateCertificateValidation(
|
||||
e.Certificate,
|
||||
e.Error.StatusCode,
|
||||
_options.AutoAcceptCertificates,
|
||||
_options.CertificateValidation);
|
||||
|
||||
if (decision.LogMessage is { Length: > 0 })
|
||||
{
|
||||
// Use the SDK's trace surface — no driver-side ILogger is plumbed today, and the
|
||||
// SDK trace is already wired up by the host. Warning level for rejections so
|
||||
// operators surface them without code changes. The non-telemetry overload is
|
||||
// marked obsolete in the latest SDK; suppress locally to keep the gateway-driver
|
||||
// surface free of an ITelemetryContext plumb-through (parity with the same
|
||||
// pattern in BuildApplicationConfigurationAsync).
|
||||
#pragma warning disable CS0618
|
||||
Opc.Ua.Utils.LogWarning(
|
||||
"OpcUaClient[{0}] cert-validation: {1} (subject={2}, status=0x{3:X8})",
|
||||
driverInstanceId, decision.LogMessage,
|
||||
e.Certificate?.Subject ?? "<null>",
|
||||
(uint)e.Error.StatusCode.Code);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
e.Accept = decision.Accept;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cert-validation decision pipeline. Pulled out as a static helper so unit tests can
|
||||
/// drive each branch without standing up an OPC UA SDK <c>CertificateValidator</c>.
|
||||
/// Order matters: revoked > SHA-1 > key-size > revocation-unknown > auto-accept-untrusted.
|
||||
/// </summary>
|
||||
/// <param name="cert">Server certificate the SDK is asking us to validate. May be null in pathological cases.</param>
|
||||
/// <param name="status">The SDK's validation result. <c>Good</c> = no failure to inspect.</param>
|
||||
/// <param name="autoAcceptUntrusted">Mirror of <see cref="OpcUaClientDriverOptions.AutoAcceptCertificates"/>.</param>
|
||||
/// <param name="opts">The cert-validation knobs.</param>
|
||||
internal static CertificateValidationDecision EvaluateCertificateValidation(
|
||||
System.Security.Cryptography.X509Certificates.X509Certificate2? cert,
|
||||
Opc.Ua.StatusCode status,
|
||||
bool autoAcceptUntrusted,
|
||||
OpcUaCertificateValidationOptions opts)
|
||||
{
|
||||
// Revoked certs are always a hard fail — never auto-accept regardless of flags.
|
||||
if (status.Code == Opc.Ua.StatusCodes.BadCertificateRevoked)
|
||||
return new CertificateValidationDecision(false, "REVOKED server certificate — rejecting");
|
||||
if (status.Code == Opc.Ua.StatusCodes.BadCertificateIssuerRevoked)
|
||||
return new CertificateValidationDecision(false, "REVOKED issuer certificate — rejecting");
|
||||
|
||||
// SHA-1 signature detection runs even when the SDK didn't surface a status —
|
||||
// we want to reject SHA-1 certs on policy, not just when the SDK happens to flag them.
|
||||
if (opts.RejectSHA1SignedCertificates && IsSha1Signed(cert))
|
||||
return new CertificateValidationDecision(false, "SHA-1 signed certificate rejected by policy");
|
||||
|
||||
// Key-size check: only meaningful for RSA keys; ECC bypasses.
|
||||
if (cert is not null && TryGetRsaKeySize(cert, out var keyBits) && keyBits < opts.MinimumCertificateKeySize)
|
||||
return new CertificateValidationDecision(false,
|
||||
$"RSA key size {keyBits} bits below minimum {opts.MinimumCertificateKeySize}");
|
||||
|
||||
// Unknown revocation status — reject only if policy says so.
|
||||
if (status.Code == Opc.Ua.StatusCodes.BadCertificateRevocationUnknown
|
||||
|| status.Code == Opc.Ua.StatusCodes.BadCertificateIssuerRevocationUnknown)
|
||||
{
|
||||
if (opts.RejectUnknownRevocationStatus)
|
||||
return new CertificateValidationDecision(false, "revocation status unknown (no/stale CRL) — rejecting per policy");
|
||||
return new CertificateValidationDecision(true, "revocation status unknown (no/stale CRL) — accepting per policy");
|
||||
}
|
||||
|
||||
// Untrusted: SDK couldn't chain the cert to a trusted issuer. Honour AutoAccept.
|
||||
if (status.Code == Opc.Ua.StatusCodes.BadCertificateUntrusted)
|
||||
{
|
||||
if (autoAcceptUntrusted) return new CertificateValidationDecision(true, null);
|
||||
return new CertificateValidationDecision(false, "untrusted certificate — rejecting (AutoAcceptCertificates=false)");
|
||||
}
|
||||
|
||||
// Anything else is an SDK-level failure — let the SDK's default disposition stand
|
||||
// (don't accept by default; surface the status code in the log).
|
||||
if (status.Code != Opc.Ua.StatusCodes.Good)
|
||||
return new CertificateValidationDecision(false, $"validation failed (status=0x{(uint)status.Code:X8})");
|
||||
|
||||
return new CertificateValidationDecision(true, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the cert's signature algorithm OID matches a SHA-1 RSA signature
|
||||
/// (<c>1.2.840.113549.1.1.5</c>) or a SHA-1 ECDSA signature (<c>1.2.840.10045.4.1</c>).
|
||||
/// Friendly-name prefix match is unreliable across .NET runtimes, so we use OIDs.
|
||||
/// </summary>
|
||||
internal static bool IsSha1Signed(System.Security.Cryptography.X509Certificates.X509Certificate2? cert)
|
||||
{
|
||||
if (cert is null) return false;
|
||||
var oid = cert.SignatureAlgorithm?.Value;
|
||||
return oid is "1.2.840.113549.1.1.5" // sha1RSA
|
||||
or "1.2.840.10045.4.1"; // sha1ECDSA
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the RSA public key size in bits if the cert has an RSA key. Returns false for
|
||||
/// non-RSA (ECC, DSA) certs so the key-size check is skipped on them.
|
||||
/// </summary>
|
||||
internal static bool TryGetRsaKeySize(
|
||||
System.Security.Cryptography.X509Certificates.X509Certificate2 cert,
|
||||
out int keyBits)
|
||||
{
|
||||
using var rsa = cert.GetRSAPublicKey();
|
||||
if (rsa is null) { keyBits = 0; return false; }
|
||||
keyBits = rsa.KeySize;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outcome of <see cref="EvaluateCertificateValidation"/>. <see cref="LogMessage"/>
|
||||
/// is null when the decision is silently "accept (Good)" — no need to log healthy
|
||||
/// validations.
|
||||
/// </summary>
|
||||
internal readonly record struct CertificateValidationDecision(bool Accept, string? LogMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the ordered failover candidate list. <c>EndpointUrls</c> wins when
|
||||
/// non-empty; otherwise fall back to <c>EndpointUrl</c> as a single-URL shortcut so
|
||||
|
||||
@@ -141,8 +141,56 @@ public sealed class OpcUaClientDriverOptions
|
||||
/// values so existing deployments see no behaviour change.
|
||||
/// </summary>
|
||||
public OpcUaSubscriptionDefaults Subscriptions { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Server-certificate validation knobs applied during the
|
||||
/// <c>CertificateValidator.CertificateValidation</c> callback. Surfaces explicit
|
||||
/// handling for revoked certs (always rejected, never auto-accepted), unknown
|
||||
/// revocation status (rejected only when <see cref="OpcUaCertificateValidationOptions.RejectUnknownRevocationStatus"/>
|
||||
/// is set), SHA-1 signature rejection, and minimum RSA key size. Defaults preserve
|
||||
/// existing behaviour wherever possible — the one tightening is
|
||||
/// <see cref="OpcUaCertificateValidationOptions.RejectSHA1SignedCertificates"/>=true
|
||||
/// since SHA-1 is spec-deprecated for OPC UA.
|
||||
/// </summary>
|
||||
public OpcUaCertificateValidationOptions CertificateValidation { get; init; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Knobs governing the server-certificate validation callback. Plumbed onto
|
||||
/// <see cref="OpcUaClientDriverOptions.CertificateValidation"/> rather than the top-level
|
||||
/// options to keep cert-related config grouped together.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>CRL discovery:</b> the OPC UA SDK reads CRL files automatically from the
|
||||
/// <c>crl/</c> sub-directory of each cert store (own, trusted, issuers). Drop the
|
||||
/// issuer's <c>.crl</c> in that folder and the SDK picks it up — no driver-side wiring
|
||||
/// required. When the directory is absent or empty, the SDK reports
|
||||
/// <c>BadCertificateRevocationUnknown</c>, which this driver gates with
|
||||
/// <see cref="RejectUnknownRevocationStatus"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="RejectSHA1SignedCertificates">
|
||||
/// Reject server certificates whose signature uses SHA-1. Default <c>true</c> — SHA-1 was
|
||||
/// deprecated by the OPC UA spec and is treated as a hard fail in production. Flip to
|
||||
/// <c>false</c> only for short-term interop with legacy controllers.
|
||||
/// </param>
|
||||
/// <param name="RejectUnknownRevocationStatus">
|
||||
/// When the SDK can't determine revocation status (no CRL present, or stale CRL),
|
||||
/// reject the cert if <c>true</c>; allow if <c>false</c>. Default <c>false</c> — many
|
||||
/// plant deployments don't run CRL infrastructure, and a hard-fail default would break
|
||||
/// them on first connection. Set <c>true</c> in environments with a managed PKI.
|
||||
/// </param>
|
||||
/// <param name="MinimumCertificateKeySize">
|
||||
/// Minimum RSA key size (bits) accepted. Certs with shorter keys are rejected. Default
|
||||
/// <c>2048</c> matches the current OPC UA spec floor; raise to 3072 or 4096 for stricter
|
||||
/// deployments. Non-RSA keys (ECC) bypass this check.
|
||||
/// </param>
|
||||
public sealed record OpcUaCertificateValidationOptions(
|
||||
bool RejectSHA1SignedCertificates = true,
|
||||
bool RejectUnknownRevocationStatus = false,
|
||||
int MinimumCertificateKeySize = 2048);
|
||||
|
||||
/// <summary>
|
||||
/// Tuning surface for OPC UA subscriptions created by <see cref="OpcUaClientDriver"/>.
|
||||
/// Lifted from the per-call hard-coded literals so operators can tune publish cadence,
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Opc.Ua;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Unit coverage for the cert-validation knobs added in PR #277. Live revocation testing
|
||||
/// requires standing up a CA + CRL; we cover the parts that are testable without one:
|
||||
/// option defaults, the static decision pipeline, SHA-1 detection, and key-size checks.
|
||||
/// </summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class OpcUaClientCertValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void Defaults_match_documented_policy()
|
||||
{
|
||||
var opts = new OpcUaClientDriverOptions();
|
||||
opts.CertificateValidation.RejectSHA1SignedCertificates.ShouldBeTrue(
|
||||
"SHA-1 is spec-deprecated for OPC UA — default must be hard-fail.");
|
||||
opts.CertificateValidation.RejectUnknownRevocationStatus.ShouldBeFalse(
|
||||
"Default must allow brownfield deployments without CRL infrastructure.");
|
||||
opts.CertificateValidation.MinimumCertificateKeySize.ShouldBe(2048);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Revoked_cert_is_rejected_even_when_AutoAccept_is_true()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateRevoked),
|
||||
autoAcceptUntrusted: true,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("REVOKED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Issuer_revoked_is_rejected_even_when_AutoAccept_is_true()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateIssuerRevoked),
|
||||
autoAcceptUntrusted: true,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("REVOKED issuer");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevocationUnknown_default_accepts_with_log_note()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateRevocationUnknown),
|
||||
autoAcceptUntrusted: false,
|
||||
new OpcUaCertificateValidationOptions { RejectUnknownRevocationStatus = false });
|
||||
|
||||
decision.Accept.ShouldBeTrue();
|
||||
decision.LogMessage!.ShouldContain("revocation status unknown");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RevocationUnknown_with_strict_flag_rejects()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateRevocationUnknown),
|
||||
autoAcceptUntrusted: true,
|
||||
new OpcUaCertificateValidationOptions { RejectUnknownRevocationStatus = true });
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("revocation status unknown");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sha1_signed_cert_is_rejected_by_default()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA1);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.Good),
|
||||
autoAcceptUntrusted: false,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("SHA-1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sha1_acceptance_can_be_opted_back_into()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA1);
|
||||
|
||||
// Untrusted + auto-accept = let it through; SHA-1 must NOT be the failing reason.
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateUntrusted),
|
||||
autoAcceptUntrusted: true,
|
||||
new OpcUaCertificateValidationOptions { RejectSHA1SignedCertificates = false });
|
||||
|
||||
decision.Accept.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Small_rsa_key_is_rejected_below_minimum()
|
||||
{
|
||||
using var cert = CreateRsaCert(1024, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.Good),
|
||||
autoAcceptUntrusted: false,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("1024");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetRsaKeySize_reports_correct_bit_count()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
OpcUaClientDriver.TryGetRsaKeySize(cert, out var bits).ShouldBeTrue();
|
||||
bits.ShouldBe(2048);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSha1Signed_detects_sha1_signature()
|
||||
{
|
||||
using var sha1Cert = CreateRsaCert(2048, HashAlgorithmName.SHA1);
|
||||
using var sha256Cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
OpcUaClientDriver.IsSha1Signed(sha1Cert).ShouldBeTrue();
|
||||
OpcUaClientDriver.IsSha1Signed(sha256Cert).ShouldBeFalse();
|
||||
OpcUaClientDriver.IsSha1Signed(null).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Untrusted_without_AutoAccept_is_rejected()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.BadCertificateUntrusted),
|
||||
autoAcceptUntrusted: false,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeFalse();
|
||||
decision.LogMessage!.ShouldContain("untrusted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Good_status_with_compliant_cert_accepts_silently()
|
||||
{
|
||||
using var cert = CreateRsaCert(2048, HashAlgorithmName.SHA256);
|
||||
|
||||
var decision = OpcUaClientDriver.EvaluateCertificateValidation(
|
||||
cert,
|
||||
new StatusCode(StatusCodes.Good),
|
||||
autoAcceptUntrusted: false,
|
||||
new OpcUaCertificateValidationOptions());
|
||||
|
||||
decision.Accept.ShouldBeTrue();
|
||||
decision.LogMessage.ShouldBeNull("Good validations shouldn't emit log noise.");
|
||||
}
|
||||
|
||||
private static X509Certificate2 CreateRsaCert(int keySize, HashAlgorithmName hash)
|
||||
{
|
||||
// .NET 10's CertificateRequest.CreateSelfSigned rejects SHA-1 outright. For the
|
||||
// SHA-256 path we use the supported API; for SHA-1 we route through a custom
|
||||
// X509SignatureGenerator that signs with SHA-1 OID so we can synthesise a SHA-1
|
||||
// signed cert in-process without shipping a binary fixture.
|
||||
var rsa = RSA.Create(keySize);
|
||||
var req = new CertificateRequest(
|
||||
new System.Security.Cryptography.X509Certificates.X500DistinguishedName(
|
||||
"CN=OpcUaClientCertValidationTests"),
|
||||
rsa,
|
||||
hash == HashAlgorithmName.SHA1 ? HashAlgorithmName.SHA256 : hash,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
if (hash == HashAlgorithmName.SHA1)
|
||||
{
|
||||
var generator = new Sha1RsaSignatureGenerator(rsa);
|
||||
var serial = new byte[8];
|
||||
System.Security.Cryptography.RandomNumberGenerator.Fill(serial);
|
||||
var built = req.Create(
|
||||
req.SubjectName,
|
||||
generator,
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddHours(1),
|
||||
serial);
|
||||
// Combine cert + key so GetRSAPublicKey works downstream.
|
||||
return built.CopyWithPrivateKey(rsa);
|
||||
}
|
||||
|
||||
return req.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddMinutes(-5),
|
||||
DateTimeOffset.UtcNow.AddHours(1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA-1 RSA signature generator. .NET 10's <see cref="X509SignatureGenerator.CreateForRSA"/>
|
||||
/// refuses SHA-1; we subclass to emit the SHA-1 RSA algorithm identifier
|
||||
/// (<c>1.2.840.113549.1.1.5</c>) and sign with SHA-1 explicitly. Test-only.
|
||||
/// </summary>
|
||||
private sealed class Sha1RsaSignatureGenerator : X509SignatureGenerator
|
||||
{
|
||||
private readonly RSA _rsa;
|
||||
public Sha1RsaSignatureGenerator(RSA rsa) { _rsa = rsa; }
|
||||
|
||||
public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm)
|
||||
{
|
||||
// DER: SEQUENCE { OID 1.2.840.113549.1.1.5, NULL }
|
||||
return new byte[]
|
||||
{
|
||||
0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x05, 0x05, 0x00,
|
||||
};
|
||||
}
|
||||
|
||||
public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm)
|
||||
=> _rsa.SignData(data, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
|
||||
|
||||
protected override PublicKey BuildPublicKey() => PublicKey.CreateFromSubjectPublicKeyInfo(
|
||||
_rsa.ExportSubjectPublicKeyInfo(), out _);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user