diff --git a/azure-pipelines-arcade-PR.yml b/azure-pipelines-arcade-PR.yml
index d9453880708..5e71f2cc5e3 100644
--- a/azure-pipelines-arcade-PR.yml
+++ b/azure-pipelines-arcade-PR.yml
@@ -210,7 +210,6 @@ stages:
# Only build and test MacOS in PR and CI builds.
- ${{ if eq(variables._RunAsPublic, True) }}:
- job: MacOS
- condition: ne(variables._RunWithCoreWcfService, True)
timeoutInMinutes: 90
pool:
name: NetCore-Public
diff --git a/eng/SendToHelix.proj b/eng/SendToHelix.proj
index a7bf7e36d1b..00a4945dd02 100644
--- a/eng/SendToHelix.proj
+++ b/eng/SendToHelix.proj
@@ -83,6 +83,21 @@
$(HelixPreCommands);export OPENSSL_ENABLE_SHA1_SIGNATURES=1
+
+
+ $(HelixPreCommands);security delete-keychain $HOME/Library/Keychains/login.keychain-db 2>/dev/null || true
+ $(HelixPreCommands);security create-keychain -p "" $HOME/Library/Keychains/login.keychain-db
+ $(HelixPreCommands);security set-keychain-settings -lut 7200 $HOME/Library/Keychains/login.keychain-db
+ $(HelixPreCommands);security unlock-keychain -p "" $HOME/Library/Keychains/login.keychain-db
+ $(HelixPreCommands);security list-keychains -d user -s $HOME/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain
+ $(HelixPreCommands);security default-keychain -d user -s $HOME/Library/Keychains/login.keychain-db
+
+
$(HelixPreCommands);set PATH=%HELIX_CORRELATION_PAYLOAD%\dotnet-cli%3B%PATH%
diff --git a/src/System.Private.ServiceModel/tests/Common/Infrastructure/CertificateManager.cs b/src/System.Private.ServiceModel/tests/Common/Infrastructure/CertificateManager.cs
index f162c9d9369..740515faa61 100644
--- a/src/System.Private.ServiceModel/tests/Common/Infrastructure/CertificateManager.cs
+++ b/src/System.Private.ServiceModel/tests/Common/Infrastructure/CertificateManager.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -233,9 +234,97 @@ public static X509Certificate2 InstallCertificateToRootStore(X509Certificate2 ce
{
// See explanation of StoreLocation selection at PlatformSpecificRootStoreLocation
certificate = AddToStoreIfNeeded(StoreName.Root, PlatformSpecificRootStoreLocation, certificate);
+
+ // On macOS, .NET's X509Store(Root, *) does not establish OS-level trust required by SslStream/SecTrust.
+ // Add an admin trust setting to the System.keychain via the `security` CLI. Passwordless sudo is set up
+ // for the Helix work item via eng/SendToHelix.proj pre-commands.
+ if ((OSHelper.Current & OSID.OSX) == OSHelper.Current)
+ {
+ AddTrustedRootOnMacOS(certificate);
+ }
+
return certificate;
}
+ private static void AddTrustedRootOnMacOS(X509Certificate2 certificate)
+ {
+ string pemPath = Path.Combine(Path.GetTempPath(), "wcf-root-" + certificate.Thumbprint + ".pem");
+ try
+ {
+ byte[] der = certificate.Export(X509ContentType.Cert);
+ var sb = new StringBuilder();
+ sb.AppendLine("-----BEGIN CERTIFICATE-----");
+ string b64 = Convert.ToBase64String(der);
+ for (int i = 0; i < b64.Length; i += 64)
+ {
+ sb.AppendLine(b64.Substring(i, Math.Min(64, b64.Length - i)));
+ }
+ sb.AppendLine("-----END CERTIFICATE-----");
+ File.WriteAllText(pemPath, sb.ToString());
+
+ // add-trusted-cert -d writes to the admin trust domain via SecTrustSettings,
+ // which on headless macOS CI runners intermittently fails with
+ // "SecTrustSettingsSetTrustSettings: The authorization was denied since no user
+ // interaction was possible." The failure is transient, so retry a few times with
+ // a short backoff; without a trusted root every TLS test would fail with
+ // UntrustedRoot.
+ const int maxAttempts = 5;
+ for (int attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ int rc = RunSecurity("sudo", "-n security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \"" + pemPath + "\"");
+ if (rc == 0)
+ {
+ break;
+ }
+
+ Console.WriteLine($"[CertificateManager] add-trusted-cert attempt {attempt}/{maxAttempts} failed (exit {rc})" + (attempt < maxAttempts ? "; retrying after 2s..." : "."));
+ if (attempt < maxAttempts)
+ {
+ System.Threading.Thread.Sleep(2000);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("[CertificateManager] Failed to add macOS admin trust: " + ex);
+ }
+ finally
+ {
+ try { if (File.Exists(pemPath)) File.Delete(pemPath); } catch { }
+ }
+ }
+
+ private static int RunSecurity(string fileName, string arguments)
+ {
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+ using (var p = Process.Start(psi))
+ {
+ string stdout = p.StandardOutput.ReadToEnd();
+ string stderr = p.StandardError.ReadToEnd();
+ p.WaitForExit(60000);
+ Console.WriteLine($"[CertificateManager] {fileName} {arguments} exit={p.ExitCode}");
+ if (!string.IsNullOrEmpty(stdout)) Console.WriteLine("[CertificateManager] stdout: " + stdout);
+ if (!string.IsNullOrEmpty(stderr)) Console.WriteLine("[CertificateManager] stderr: " + stderr);
+ return p.ExitCode;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[CertificateManager] Failed to run '{fileName} {arguments}': {ex}");
+ return -1;
+ }
+ }
+
// Install the certificate into the My store.
// It will not install the certificate if it is already present in the store.
// It returns the thumbprint of the certificate, regardless whether it was added or found.
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/UDS/UDSBindingTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/UDS/UDSBindingTests.cs
index ab9add0b076..7d7c5eabe21 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/UDS/UDSBindingTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/UDS/UDSBindingTests.cs
@@ -93,7 +93,6 @@ public void WindowsAuth()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(SSL_Available))]
[OuterLoop]
private void BasicCertAsTransport()
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/FederationHttp/WSFederationHttpBindingTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/FederationHttp/WSFederationHttpBindingTests.cs
index 6cdbea3edb7..ccb1b4ca528 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/FederationHttp/WSFederationHttpBindingTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/FederationHttp/WSFederationHttpBindingTests.cs
@@ -14,7 +14,6 @@
public class WSFederationHttpBindingTests : ConditionalWcfTest
{
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available),
@@ -76,7 +75,6 @@ public static void WSFederationHttpBindingTests_Succeeds(MessageSecurityVersion
}
}
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available),
@@ -128,7 +126,6 @@ public static void WSTrustTokeParameters_WSStaticHelper()
}
}
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available),
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpTransportWithMessageCredentialSecurityTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpTransportWithMessageCredentialSecurityTests.cs
index e31279c9777..7e22ce5a1b5 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpTransportWithMessageCredentialSecurityTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpTransportWithMessageCredentialSecurityTests.cs
@@ -8,7 +8,6 @@
public class BasicHttpTransportWithMessageCredentialSecurityTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
@@ -56,7 +55,6 @@ public static void BasicHttps_SecModeTransWithMessCred_CertClientCredential_Succ
}
[WcfTheory]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
@@ -107,7 +105,6 @@ public static void BasicHttps_SecModeTransWithMessCred_UserNameClientCredential_
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
@@ -156,7 +153,6 @@ public static void Https_SecModeTransWithMessCred_UserNameClientCredential_Succe
}
[WcfTheory]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpsTransportWithMessageCredentialSecurityTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpsTransportWithMessageCredentialSecurityTests.cs
index 73d82046587..f4908533e70 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpsTransportWithMessageCredentialSecurityTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/BasicHttpsTransportWithMessageCredentialSecurityTests.cs
@@ -7,7 +7,6 @@
public class BasicHttpsTransportWithMessageCredentialSecurityTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WS2007HttpTransportWithMessageCredentialsSecurityTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WS2007HttpTransportWithMessageCredentialsSecurityTests.cs
index bd8d7787d3e..c96cad18fb8 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WS2007HttpTransportWithMessageCredentialsSecurityTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WS2007HttpTransportWithMessageCredentialsSecurityTests.cs
@@ -12,7 +12,6 @@
public class WS2007HttpTransportWithMessageCredentialsSecurityTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSHttpTransportWithMessageCredentialSecurityTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSHttpTransportWithMessageCredentialSecurityTests.cs
index 47be25bd29a..55f657d8366 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSHttpTransportWithMessageCredentialSecurityTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSHttpTransportWithMessageCredentialSecurityTests.cs
@@ -13,7 +13,6 @@
public class WSHttpTransportWithMessageCredentialSecurityTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSNetTcpTransportWithMessageCredentialSecurityTests.cs b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSNetTcpTransportWithMessageCredentialSecurityTests.cs
index fb249b72670..f9df8912542 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSNetTcpTransportWithMessageCredentialSecurityTests.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Binding/WS/TransportWithMessageCredentialSecurity/WSNetTcpTransportWithMessageCredentialSecurityTests.cs
@@ -15,7 +15,6 @@
public class WSNetTcpTransportWithMessageCredentialSecurityTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
@@ -37,6 +36,15 @@ public static void NetTcp_SecModeTransWithMessCred_CertClientCredential_Succeeds
clientCertThumb = ServiceUtilHelper.ClientCertificate.Thumbprint;
factory = new ChannelFactory(binding, endpointAddress);
+ // macOS SecTrust cannot obtain a positive revocation response for a
+ // private CA (dotnet/runtime#31249); the transport-cert revocation
+ // check is incidental to this message-credential scenario, so skip
+ // it on macOS only.
+ if (OSID.OSX.MatchesCurrent())
+ {
+ factory.Credentials.ServiceCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck;
+ }
+
factory.Credentials.ClientCertificate.SetCertificate(
StoreLocation.CurrentUser,
StoreName.My,
@@ -63,7 +71,6 @@ public static void NetTcp_SecModeTransWithMessCred_CertClientCredential_Succeeds
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
@@ -85,6 +92,15 @@ public static void NetTcp_SecModeTransWithMessCred_UserNameClientCredential_Succ
endpointAddress = new EndpointAddress(new Uri(Endpoints.Tcp_SecModeTransWithMessCred_ClientCredTypeUserName));
factory = new ChannelFactory(binding, endpointAddress);
+ // macOS SecTrust cannot obtain a positive revocation response for a
+ // private CA (dotnet/runtime#31249); the transport-cert revocation
+ // check is incidental to this message-credential scenario, so skip
+ // it on macOS only.
+ if (OSID.OSX.MatchesCurrent())
+ {
+ factory.Credentials.ServiceCertificate.Authentication.RevocationMode = X509RevocationMode.NoCheck;
+ }
+
username = Guid.NewGuid().ToString("n").Substring(0, 8);
char[] usernameArr = username.ToCharArray();
Array.Reverse(usernameArr);
@@ -112,7 +128,6 @@ public static void NetTcp_SecModeTransWithMessCred_UserNameClientCredential_Succ
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.0.cs b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.0.cs
index 28667bb5919..0cd81919870 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.0.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.0.cs
@@ -18,7 +18,6 @@ public partial class HttpsTests : ConditionalWcfTest
[WcfTheory]
[InlineData(WSMessageEncoding.Text)]
[InlineData(WSMessageEncoding.Mtom)]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(Server_Accepts_Certificates),
@@ -221,7 +220,6 @@ public static void SameBinding_Soap12_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
@@ -269,7 +267,6 @@ public static void ServerCertificateValidation_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(SSL_Available))]
[OuterLoop]
public static async Task ServerCertificateValidationUsingIdentity_EchoString()
@@ -310,7 +307,6 @@ public static async Task ServerCertificateValidationUsingIdentity_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Client_Certificate_Installed),
nameof(SSL_Available))]
[OuterLoop]
@@ -348,7 +344,6 @@ public static void ServerCertificateValidationUsingIdentity_Throws_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(Server_Accepts_Certificates),
@@ -398,7 +393,6 @@ public static void ClientCertificate_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
nameof(Server_Accepts_Certificates),
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.1.cs b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.1.cs
index 86f10638b6a..f21420087b4 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.1.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Https/HttpsTests.4.1.1.cs
@@ -4,6 +4,7 @@
using System;
+using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Security;
using Infrastructure.Common;
@@ -171,6 +172,11 @@ public static void Https_SecModeTrans_CertValMode_PeerOrChainTrust_Succeeds_Chai
}
[WcfFact]
+ // macOS SecTrust cannot obtain a positive revocation response for a private
+ // CA under X509RevocationMode.Online (dotnet/runtime#31249), so a ChainTrust
+ // validation of the server cert fails on macOS. This test's purpose is the
+ // ChainTrust validation itself, so it is skipped on macOS rather than having
+ // its revocation check disabled.
[Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed),
nameof(Client_Certificate_Installed),
diff --git a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Tcp/ClientCredentialTypeTests.4.1.0.cs b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Tcp/ClientCredentialTypeTests.4.1.0.cs
index 5a59d9fa9ae..53d2f089c96 100644
--- a/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Tcp/ClientCredentialTypeTests.4.1.0.cs
+++ b/src/System.Private.ServiceModel/tests/Scenarios/Security/TransportSecurity/Tcp/ClientCredentialTypeTests.4.1.0.cs
@@ -13,7 +13,6 @@
public partial class Tcp_ClientCredentialTypeTests : ConditionalWcfTest
{
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed), nameof(Client_Certificate_Installed))]
[OuterLoop]
public static void TcpClientCredentialType_Certificate_EchoString()
@@ -62,7 +61,6 @@ public static void TcpClientCredentialType_Certificate_EchoString()
}
[WcfFact]
- [Issue(2870, OS = OSID.OSX)]
[Condition(nameof(Root_Certificate_Installed), nameof(Client_Certificate_Installed))]
[OuterLoop]
public static void TcpClientCredentialType_Certificate_CustomValidator_EchoString()
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGenerator/CertificateGenerator.csproj b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGenerator/CertificateGenerator.csproj
index aa0f59bf3c2..11d131819bf 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGenerator/CertificateGenerator.csproj
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGenerator/CertificateGenerator.csproj
@@ -1,7 +1,7 @@
- net471
+ net10.0
Exe
@@ -10,7 +10,7 @@
-
+
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateCreationSettings.cs b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateCreationSettings.cs
index 542681946c7..57f989e3cd1 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateCreationSettings.cs
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateCreationSettings.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
-using Org.BouncyCastle.Asn1.X509;
namespace WcfTestCommon
{
@@ -24,7 +23,8 @@ public CertificateCreationSettings()
public DateTime ValidityNotAfter { get; set; }
public CertificateValidityType ValidityType { get; set; }
public bool IncludeCrlDistributionPoint { get; set; } = true;
- public List EKU { get; set; }
+ // List of EKU OIDs (e.g., "1.3.6.1.5.5.7.3.1" for serverAuth, "1.3.6.1.5.5.7.3.2" for clientAuth).
+ public List EKU { get; set; }
}
[Serializable]
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGenerator.cs b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGenerator.cs
index a663857336e..2270f8a8101 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGenerator.cs
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGenerator.cs
@@ -2,31 +2,21 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.IO;
+using System.Formats.Asn1;
using System.Linq;
+using System.Numerics;
using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
-using Org.BouncyCastle.Asn1;
-using Org.BouncyCastle.Asn1.X509;
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Generators;
-using Org.BouncyCastle.Crypto.Operators;
-using Org.BouncyCastle.Crypto.Prng;
-using Org.BouncyCastle.Math;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
-using Org.BouncyCastle.X509;
-using Org.BouncyCastle.X509.Extension;
-using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
-using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags;
namespace WcfTestCommon
{
- // NOT THREADSAFE. Callers should lock before doing work with this class if multithreaded operation is expected
+ // NOT THREADSAFE. Callers should lock before doing work with this class if multithreaded operation is expected.
+ // This generator uses the .NET built-in X.509 APIs (CertificateRequest / RSA) so the produced
+ // DER encodings are compatible with all platform X.509 stacks (including macOS Apple Security framework).
public class CertificateGenerator
{
private bool _isInitialized;
@@ -39,85 +29,77 @@ public class CertificateGenerator
private TimeSpan _validityPeriod = TimeSpan.FromDays(1);
// This can't be too short as there might be a time skew between machines,
- // but also can't be too long, as the CRL is cached by the machine
- private TimeSpan _crlValidityGracePeriodStart = TimeSpan.FromMinutes(5);
+ // but also can't be too long, as the CRL is cached by the machine.
private TimeSpan _crlValidityGracePeriodEnd = TimeSpan.FromMinutes(5);
// Give the cert a grace period in case there's a time skew between machines
private readonly TimeSpan _gracePeriod = TimeSpan.FromHours(1);
private readonly string _authorityCanonicalName = "DO_NOT_TRUST_WcfBridgeRootCA";
- private readonly string _signatureAlgorithm = Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id;
- private readonly string _upnObjectId = "1.3.6.1.4.1.311.20.2.3";
private readonly int _keyLengthInBits = 2048;
- private static readonly X509V3CertificateGenerator s_certGenerator = new X509V3CertificateGenerator();
- private static readonly X509V2CrlGenerator s_crlGenerator = new X509V2CrlGenerator();
-
- // key: serial number, value: revocation time
+ // key: serial number (lowercase hex), value: revocation time
private static Dictionary s_revokedCertificates = new Dictionary();
- private RsaKeyPairGenerator _keyPairGenerator;
- private SecureRandom _random;
-
private DateTime _initializationDateTime;
private DateTime _defaultValidityNotBefore;
private DateTime _defaultValidityNotAfter;
- // We need to hang onto the _authorityKeyPair and _authorityCertificate - all certificates generated
- // by this instance will be signed by this Authority certificate and private key
- private AsymmetricCipherKeyPair _authorityKeyPair;
+ // Authority private key + cert (with private key) used to sign all issued certificates
+ private RSA _authorityKey;
+ private X509Certificate2 _authorityCertWithKey;
private X509CertificateContainer _authorityCertificate;
public void Initialize()
{
- if (!_isInitialized)
+ if (_isInitialized)
{
- if (string.IsNullOrWhiteSpace(_authorityCanonicalName))
- {
- throw new ArgumentException("AuthorityCanonicalName must not be an empty string or only whitespace", "AuthorityCanonicalName");
- }
+ return;
+ }
- if (string.IsNullOrWhiteSpace(_password))
- {
- throw new ArgumentException("Password must not be an empty string or only whitespace", "Password");
- }
+ if (string.IsNullOrWhiteSpace(_authorityCanonicalName))
+ {
+ throw new ArgumentException("AuthorityCanonicalName must not be an empty string or only whitespace", "AuthorityCanonicalName");
+ }
- Uri dummy;
- if (string.IsNullOrWhiteSpace(_crlUriRelativePath) && !Uri.TryCreate(_crlUriRelativePath, UriKind.Relative, out dummy))
- {
- throw new ArgumentException("CrlUri must be a valid relative URI", "CrlUriRelativePath");
- }
+ if (string.IsNullOrWhiteSpace(_password))
+ {
+ throw new ArgumentException("Password must not be an empty string or only whitespace", "Password");
+ }
- _crlUri = new Uri(string.Format("http://{0}{1}", _crlServiceUri, _crlUriRelativePath)).AbsoluteUri;
+ Uri dummy;
+ if (string.IsNullOrWhiteSpace(_crlUriRelativePath) && !Uri.TryCreate(_crlUriRelativePath, UriKind.Relative, out dummy))
+ {
+ throw new ArgumentException("CrlUri must be a valid relative URI", "CrlUriRelativePath");
+ }
- _initializationDateTime = DateTime.UtcNow;
- _defaultValidityNotBefore = _initializationDateTime.Subtract(_gracePeriod);
- _defaultValidityNotAfter = _initializationDateTime.Add(_validityPeriod);
+ _crlUri = new Uri(string.Format("http://{0}{1}", _crlServiceUri, _crlUriRelativePath)).AbsoluteUri;
- _random = new SecureRandom(new CryptoApiRandomGenerator());
- _keyPairGenerator = new RsaKeyPairGenerator();
- _keyPairGenerator.Init(new KeyGenerationParameters(_random, _keyLengthInBits));
- _authorityKeyPair = _keyPairGenerator.GenerateKeyPair();
+ _initializationDateTime = DateTime.UtcNow;
+ _defaultValidityNotBefore = _initializationDateTime.Subtract(_gracePeriod);
+ _defaultValidityNotAfter = _initializationDateTime.Add(_validityPeriod);
- _isInitialized = true;
+ _isInitialized = true;
- Trace.WriteLine("[CertificateGenerator] initialized with the following configuration:");
- Trace.WriteLine(string.Format(" {0} = {1}", "AuthorityCanonicalName", _authorityCanonicalName));
- Trace.WriteLine(string.Format(" {0} = {1}", "CrlUri", _crlUri));
- Trace.WriteLine(string.Format(" {0} = {1}", "Password", _password));
- Trace.WriteLine(string.Format(" {0} = {1}", "ValidityPeriod", _validityPeriod));
- Trace.WriteLine(string.Format(" {0} = {1}", "Valid to", _defaultValidityNotAfter));
+ Trace.WriteLine("[CertificateGenerator] initialized with the following configuration:");
+ Trace.WriteLine(string.Format(" {0} = {1}", "AuthorityCanonicalName", _authorityCanonicalName));
+ Trace.WriteLine(string.Format(" {0} = {1}", "CrlUri", _crlUri));
+ Trace.WriteLine(string.Format(" {0} = {1}", "Password", _password));
+ Trace.WriteLine(string.Format(" {0} = {1}", "ValidityPeriod", _validityPeriod));
+ Trace.WriteLine(string.Format(" {0} = {1}", "Valid to", _defaultValidityNotAfter));
- _authorityCertificate = CreateCertificate(isAuthority: true, isMachineCert: false, signingCertificate: null, certificateCreationSettings: null);
- }
+ _authorityCertificate = CreateCertificate(isAuthority: true, isMachineCert: false, signingCertificate: null, certificateCreationSettings: null);
}
public void Reset()
{
- s_certGenerator.Reset();
- s_crlGenerator.Reset();
_authorityCertificate = null;
+ _authorityCertWithKey = null;
+ if (_authorityKey != null)
+ {
+ _authorityKey.Dispose();
+ _authorityKey = null;
+ }
_isInitialized = false;
}
@@ -135,7 +117,7 @@ public byte[] CrlEncoded
get
{
EnsureInitialized();
- return CreateCrl(_authorityCertificate.InternalCertificate).GetEncoded();
+ return CreateCrl();
}
}
@@ -154,7 +136,7 @@ public string AuthorityDistinguishedName
get
{
EnsureInitialized();
- return CreateX509Name(_authorityCanonicalName).ToString();
+ return BuildDistinguishedName(_authorityCanonicalName).Name;
}
}
@@ -179,10 +161,7 @@ public string CrlUri
public string CrlServiceUri
{
- get
- {
- return _crlServiceUri;
- }
+ get { return _crlServiceUri; }
set
{
EnsureNotInitialized("CrlServiceUri");
@@ -192,10 +171,7 @@ public string CrlServiceUri
public string CrlUriRelativePath
{
- get
- {
- return _crlUriRelativePath;
- }
+ get { return _crlUriRelativePath; }
set
{
EnsureNotInitialized("CrlUriRelativePath");
@@ -205,11 +181,7 @@ public string CrlUriRelativePath
public List RevokedCertificates
{
- get
- {
- List retVal = new List(s_revokedCertificates.Keys);
- return retVal;
- }
+ get { return new List(s_revokedCertificates.Keys); }
}
public TimeSpan ValidityPeriod
@@ -225,36 +197,45 @@ public TimeSpan ValidityPeriod
public X509CertificateContainer CreateMachineCertificate(CertificateCreationSettings creationSettings)
{
EnsureInitialized();
- return CreateCertificate(false, true, _authorityCertificate.InternalCertificate, creationSettings);
+ return CreateCertificate(false, true, _authorityCertWithKey, creationSettings);
}
public X509CertificateContainer CreateUserCertificate(CertificateCreationSettings creationSettings)
{
EnsureInitialized();
- return CreateCertificate(false, false, _authorityCertificate.InternalCertificate, creationSettings);
+ return CreateCertificate(false, false, _authorityCertWithKey, creationSettings);
}
public static BigInteger HashFriendlyName(string input)
{
using (SHA256 sha256 = SHA256.Create())
{
- // Convert the input string to a byte array
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
-
- // Compute the hash value of the input bytes, and take the first 20 bytes
+ // Take first 20 bytes (160 bits) to fit in a typical serial number range
byte[] hashBytes = sha256.ComputeHash(inputBytes).Take(20).ToArray();
- // return a Positive BigInt of the hash
- var bigInteger = new BigInteger(1, hashBytes.ToArray());
- return bigInteger;
+ // Force a positive BigInteger by appending a zero byte if the high bit is set
+ if ((hashBytes[0] & 0x80) != 0)
+ {
+ byte[] padded = new byte[hashBytes.Length + 1];
+ Array.Copy(hashBytes, 0, padded, 1, hashBytes.Length);
+ hashBytes = padded;
+ }
+
+ // BigInteger ctor expects little-endian; reverse for big-endian hash
+ Array.Reverse(hashBytes);
+ return new BigInteger(hashBytes);
}
}
- public static string HashFriendlyNameToString(string input) => HashFriendlyName(input).ToString(16).ToUpper();
+ public static string HashFriendlyNameToString(string input)
+ {
+ return HashFriendlyName(input).ToString("X").TrimStart('0');
+ }
- // Only the ctor should be calling with isAuthority = true
- // if isAuthority, value for isMachineCert doesn't matter
- private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMachineCert, X509Certificate signingCertificate, CertificateCreationSettings certificateCreationSettings)
+ // Only Initialize() should be calling with isAuthority = true.
+ // If isAuthority, value for isMachineCert doesn't matter.
+ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMachineCert, X509Certificate2 signingCertificate, CertificateCreationSettings certificateCreationSettings)
{
if (certificateCreationSettings == null)
{
@@ -268,7 +249,6 @@ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMach
}
}
- // Set to default cert creation settings if not set
if (certificateCreationSettings.ValidityNotBefore == default(DateTime))
{
certificateCreationSettings.ValidityNotBefore = _defaultValidityNotBefore;
@@ -278,6 +258,28 @@ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMach
certificateCreationSettings.ValidityNotAfter = _defaultValidityNotAfter;
}
+ // The authority cert needs a validity window wide enough to contain every child cert (including
+ // intentionally expired ones with NotBefore in the past). Unlike BouncyCastle, .NET's
+ // CertificateRequest.Create(issuer, notBefore, notAfter, ...) enforces that the issued cert's
+ // window is contained within the issuer's. Use a generous ±10 year window for the authority.
+ if (isAuthority)
+ {
+ certificateCreationSettings.ValidityNotBefore = _initializationDateTime.AddYears(-10);
+ certificateCreationSettings.ValidityNotAfter = _initializationDateTime.AddYears(10);
+ }
+ else if (signingCertificate != null)
+ {
+ // Defensive clamp in case a caller passes dates outside the issuer window.
+ if (certificateCreationSettings.ValidityNotBefore < signingCertificate.NotBefore.ToUniversalTime())
+ {
+ certificateCreationSettings.ValidityNotBefore = signingCertificate.NotBefore.ToUniversalTime();
+ }
+ if (certificateCreationSettings.ValidityNotAfter > signingCertificate.NotAfter.ToUniversalTime())
+ {
+ certificateCreationSettings.ValidityNotAfter = signingCertificate.NotAfter.ToUniversalTime();
+ }
+ }
+
if (!isAuthority ^ (signingCertificate != null))
{
throw new ArgumentException("Either isAuthority == true or signingCertificate is not null");
@@ -285,8 +287,8 @@ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMach
string subject = certificateCreationSettings.Subject;
// If certificateCreationSettings.SubjectAlternativeNames == null, then we should add exactly one SubjectAlternativeName == Subject
- // so that the default certificate generated is compatible with mainline scenarios
- // However, if certificateCreationSettings.SubjectAlternativeNames == string[0], then allow this as this is a legit scenario we want to test out
+ // so that the default certificate generated is compatible with mainline scenarios.
+ // However, if certificateCreationSettings.SubjectAlternativeNames == string[0], then allow this as this is a legit scenario we want to test out.
if (certificateCreationSettings.SubjectAlternativeNames == null)
{
certificateCreationSettings.SubjectAlternativeNames = new string[1] { subject };
@@ -301,208 +303,214 @@ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMach
EnsureInitialized();
- s_certGenerator.Reset();
+ // Tag on the generation time to prevent caching of the cert CRL on Linux
+ X500DistinguishedName authorityDn = BuildDistinguishedName(string.Format("{0} {1}", _authorityCanonicalName, DateTime.Now.ToString("s")));
- // Tag on the generation time to prevent caching of the cert CRL in Linux
- X509Name authorityX509Name = CreateX509Name(string.Format("{0} {1}", _authorityCanonicalName, DateTime.Now.ToString("s")));
- BigInteger serialNum;
+ byte[] serialNum = ComputeSerialNumber(certificateCreationSettings);
- // Search by serial number in Linux/MacOS
- if (!CertificateHelper.CurrentOperatingSystem.IsWindows() && certificateCreationSettings.FriendlyName != null)
- {
- serialNum = HashFriendlyName(certificateCreationSettings.FriendlyName);
- }
- else
- {
- serialNum = new BigInteger(64 /*sizeInBits*/, _random).Abs();
- }
+ RSA subjectKey = isAuthority ? (_authorityKey = RSA.Create(_keyLengthInBits)) : RSA.Create(_keyLengthInBits);
+
+ X500DistinguishedName subjectDn;
+ CertificateRequest req;
- var keyPair = isAuthority ? _authorityKeyPair : _keyPairGenerator.GenerateKeyPair();
if (isAuthority)
{
- s_certGenerator.SetIssuerDN(authorityX509Name);
- s_certGenerator.SetSubjectDN(authorityX509Name);
+ subjectDn = authorityDn;
+ req = new CertificateRequest(subjectDn, subjectKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
- var authorityKeyIdentifier = new AuthorityKeyIdentifier(
- SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(_authorityKeyPair.Public),
- new GeneralNames(new GeneralName(authorityX509Name)),
- serialNum);
-
- s_certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, authorityKeyIdentifier);
- s_certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(X509KeyUsage.DigitalSignature | X509KeyUsage.KeyAgreement | X509KeyUsage.KeyCertSign | X509KeyUsage.KeyEncipherment | X509KeyUsage.CrlSign));
+ req.CertificateExtensions.Add(new X509BasicConstraintsExtension(certificateAuthority: true, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true));
+ req.CertificateExtensions.Add(new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyAgreement | X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.CrlSign,
+ critical: false));
}
else
{
- X509Name subjectName = CreateX509Name(subject);
- s_certGenerator.SetIssuerDN(signingCertificate.SubjectDN);
- s_certGenerator.SetSubjectDN(subjectName);
+ subjectDn = BuildDistinguishedName(subject);
+ req = new CertificateRequest(subjectDn, subjectKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
- s_certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(_authorityKeyPair.Public));
- s_certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(X509KeyUsage.DigitalSignature | X509KeyUsage.KeyAgreement | X509KeyUsage.KeyEncipherment));
+ req.CertificateExtensions.Add(new X509BasicConstraintsExtension(certificateAuthority: false, hasPathLengthConstraint: false, pathLengthConstraint: 0, critical: true));
+ req.CertificateExtensions.Add(new X509KeyUsageExtension(
+ X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyAgreement | X509KeyUsageFlags.KeyEncipherment,
+ critical: false));
}
- s_certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(keyPair.Public));
+ // SubjectKeyIdentifier
+ X509SubjectKeyIdentifierExtension ski = new X509SubjectKeyIdentifierExtension(req.PublicKey, critical: false);
+ req.CertificateExtensions.Add(ski);
- s_certGenerator.SetSerialNumber(serialNum);
- s_certGenerator.SetNotBefore(certificateCreationSettings.ValidityNotBefore);
- s_certGenerator.SetNotAfter(certificateCreationSettings.ValidityNotAfter);
- s_certGenerator.SetPublicKey(keyPair.Public);
+ // AuthorityKeyIdentifier
+ byte[] authorityKeyId = isAuthority
+ ? HexToBytes(ski.SubjectKeyIdentifier)
+ : GetSubjectKeyIdentifierBytes(signingCertificate);
+ req.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromSubjectKeyIdentifier(authorityKeyId));
- s_certGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(isAuthority));
+ // Extended Key Usage
+ OidCollection ekuOids = new OidCollection();
if (certificateCreationSettings.EKU == null || certificateCreationSettings.EKU.Count == 0)
{
- s_certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth));
+ ekuOids.Add(Oids.ServerAuthEkuOid);
+ ekuOids.Add(Oids.ClientAuthEkuOid);
}
else
{
- s_certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(certificateCreationSettings.EKU));
+ foreach (string ekuOid in certificateCreationSettings.EKU)
+ {
+ ekuOids.Add(new Oid(ekuOid));
+ }
+ }
+ if (ekuOids.Count > 0)
+ {
+ req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(ekuOids, critical: false));
}
+ // Subject Alternative Name
if (!isAuthority)
{
if (isMachineCert)
{
- List subjectAlternativeNamesAsAsn1EncodableList = new List();
-
- // All endpoints should also be in the Subject Alt Names
- for (int i = 0; i < subjectAlternativeNames.Length; i++)
+ SubjectAlternativeNameBuilder sanBuilder = new SubjectAlternativeNameBuilder();
+ bool any = false;
+ foreach (string san in subjectAlternativeNames)
{
- if (!string.IsNullOrWhiteSpace(subjectAlternativeNames[i]))
+ if (!string.IsNullOrWhiteSpace(san))
{
- // Machine certs can have additional DNS names
- subjectAlternativeNamesAsAsn1EncodableList.Add(new GeneralName(GeneralName.DnsName, subjectAlternativeNames[i]));
+ sanBuilder.AddDnsName(san);
+ any = true;
}
}
-
- s_certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, true, new DerSequence(subjectAlternativeNamesAsAsn1EncodableList.ToArray()));
+ if (any)
+ {
+ // SubjectAlternativeNameBuilder.Build defaults to non-critical; rebuild as critical.
+ X509Extension defaultSan = sanBuilder.Build(critical: true);
+ req.CertificateExtensions.Add(defaultSan);
+ }
}
else
{
+ // User cert: skip the first SAN (which mirrors Subject) and emit remaining as UPN OtherName entries.
if (subjectAlternativeNames.Length > 1)
{
- var subjectAlternativeNamesAsAsn1EncodableList = new Asn1EncodableVector();
-
- // Only add a SAN for the user if there are any
+ SubjectAlternativeNameBuilder upnBuilder = new SubjectAlternativeNameBuilder();
+ bool anyUpn = false;
for (int i = 1; i < subjectAlternativeNames.Length; i++)
{
if (!string.IsNullOrWhiteSpace(subjectAlternativeNames[i]))
{
- Asn1EncodableVector otherNames = new Asn1EncodableVector();
- otherNames.Add(new DerObjectIdentifier(_upnObjectId));
- otherNames.Add(new DerTaggedObject(true, 0, new DerUtf8String(subjectAlternativeNames[i])));
-
- Asn1Object genName = new DerTaggedObject(false, 0, new DerSequence(otherNames));
-
- subjectAlternativeNamesAsAsn1EncodableList.Add(genName);
+ upnBuilder.AddUserPrincipalName(subjectAlternativeNames[i]);
+ anyUpn = true;
}
}
- s_certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, true, new DerSequence(subjectAlternativeNamesAsAsn1EncodableList));
+ if (anyUpn)
+ {
+ req.CertificateExtensions.Add(upnBuilder.Build(critical: true));
+ }
}
}
}
+ // CRL Distribution Points
if (isAuthority || certificateCreationSettings.IncludeCrlDistributionPoint)
{
- var crlDistributionPoints = new DistributionPoint[1]
- {
- new DistributionPoint(
- new DistributionPointName(
- new GeneralNames(
- new GeneralName(
- GeneralName.UniformResourceIdentifier, string.Format("{0}", _crlUri, serialNum.ToString(radix: 16))))),
- null,
- null)
- };
- var revocationListExtension = new CrlDistPoint(crlDistributionPoints);
- s_certGenerator.AddExtension(X509Extensions.CrlDistributionPoints, false, revocationListExtension);
+ req.CertificateExtensions.Add(BuildCrlDistributionPointsExtension(_crlUri));
}
- ISignatureFactory signatureFactory = new Asn1SignatureFactory(_signatureAlgorithm, _authorityKeyPair.Private, _random);
- X509Certificate cert = s_certGenerator.Generate(signatureFactory);
+ X509Certificate2 cert;
+ if (isAuthority)
+ {
+ cert = req.CreateSelfSigned(certificateCreationSettings.ValidityNotBefore, certificateCreationSettings.ValidityNotAfter);
+ }
+ else
+ {
+ cert = req.Create(signingCertificate, certificateCreationSettings.ValidityNotBefore, certificateCreationSettings.ValidityNotAfter, serialNum);
+ }
+
+ // Build a complete X509Certificate2 with the private key attached.
+ // CreateSelfSigned already attaches the private key; req.Create(issuer,...) does not.
+ X509Certificate2 certWithKey = cert.HasPrivateKey ? cert : cert.CopyWithPrivateKey(subjectKey);
+
+ // For consistency with previous behavior, always export the cert with private key as PFX.
+ // X509KeyStorageFlags.Exportable lets callers re-export later.
+ byte[] pfxBytes = certWithKey.Export(X509ContentType.Pkcs12, _password);
+
+ // On Windows (and Linux), round-trip through X509CertificateLoader.LoadPkcs12 with
+ // PersistKeySet so the private key lands in the platform key container; otherwise the
+ // in-memory ephemeral key from CopyWithPrivateKey won't survive being added to a cert
+ // store and lookups later return a cert with no usable private key.
+ //
+ // On macOS, LoadPkcs12 fails with "The specified keychain could not be found" because
+ // the Apple loader needs a keychain handle to attach the key. CertificateManager bypasses
+ // X509Store on macOS (uses the security CLI), so the in-memory CopyWithPrivateKey result
+ // works there.
+ X509Certificate2 outputCert;
+ if (CertificateHelper.CurrentOperatingSystem.IsMacOS())
+ {
+ outputCert = certWithKey;
+ }
+ else
+ {
+ outputCert = X509CertificateLoader.LoadPkcs12(
+ pfxBytes,
+ _password,
+ X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
+ }
+
+ // Set FriendlyName on Windows so lookups via CertificateFromFriendlyName succeed.
+ // The setter throws PlatformNotSupportedException on non-Windows; the macOS/Linux
+ // lookup paths fall through to a deterministic-serial match instead.
+ if (CertificateHelper.CurrentOperatingSystem.IsWindows()
+ && !string.IsNullOrEmpty(certificateCreationSettings.FriendlyName))
+ {
+#pragma warning disable CA1416 // Validate platform compatibility (guarded by IsWindows())
+ outputCert.FriendlyName = certificateCreationSettings.FriendlyName;
+#pragma warning restore CA1416
+ }
switch (certificateCreationSettings.ValidityType)
{
case CertificateValidityType.Revoked:
- RevokeCertificateBySerialNumber(serialNum.ToString(radix: 16));
+ RevokeCertificateBySerialNumber(SerialToHex(serialNum));
break;
case CertificateValidityType.Expired:
break;
default:
- EnsureCertificateIsValid(cert);
+ EnsureCertificateIsValid(outputCert);
break;
}
- // For now, given that we don't know what format to return it in, preserve the formats so we have
- // the flexibility to do what we need to
-
- X509CertificateContainer container = new X509CertificateContainer();
-
- X509CertificateEntry[] chain = new X509CertificateEntry[1];
- chain[0] = new X509CertificateEntry(cert);
-
- Pkcs12Store store = new Pkcs12StoreBuilder().Build();
- store.SetKeyEntry(
- certificateCreationSettings.FriendlyName != null ? certificateCreationSettings.FriendlyName : string.Empty,
- new AsymmetricKeyEntry(keyPair.Private),
- chain);
-
- using (MemoryStream stream = new MemoryStream())
+ X509CertificateContainer container = new X509CertificateContainer
{
- store.Save(stream, _password.ToCharArray(), _random);
- container.Pfx = stream.ToArray();
- }
-
- X509Certificate2 outputCert = null;
+ Subject = subject,
+ Pfx = pfxBytes,
+ Certificate = outputCert,
+ Thumbprint = outputCert.Thumbprint
+ };
if (isAuthority)
{
- // don't hand out the private key for the cert when it's the authority
- outputCert = new X509Certificate2(cert.GetEncoded());
+ _authorityCertWithKey = certWithKey;
+ // certWithKey is the same instance as cert (CreateSelfSigned attaches the key in-place); do not dispose.
+ // On non-macOS, outputCert is a separate (round-tripped) instance owned by the container.
}
else
{
- // Otherwise, allow encode with the private key. note that X509Certificate2.RawData will not provide the private key
- // you will have to re-export this cert if needed
- if (CertificateHelper.CurrentOperatingSystem.IsMacOS())
+ // Dispose the keyless original if it's a separate instance.
+ if (!ReferenceEquals(cert, certWithKey))
{
- //string tempKeychainFilePath = Path.GetTempFileName();
- string tempKeychainFilePath = Path.Combine(Environment.CurrentDirectory, Path.GetRandomFileName());
- System.Security.Cryptography.X509Certificates.X509Store MacOsTempStore = CertificateHelper.GetMacOSX509Store(tempKeychainFilePath);
- MacOsTempStore.Certificates.Import(container.Pfx, _password, X509KeyStorageFlags.Exportable);
- MacOsTempStore.Close();
- MacOsTempStore.Dispose();
-
- MacOsTempStore = CertificateHelper.GetMacOSX509Store(tempKeychainFilePath);
-
- outputCert = ((IEnumerable)MacOsTempStore.Certificates).FirstOrDefault();
-
- if (outputCert == null)
- {
- Console.WriteLine("Couldn't find Certificate..");
- }
-
- MacOsTempStore.Dispose();
- if (File.Exists(tempKeychainFilePath))
- {
- File.Delete(tempKeychainFilePath);
- }
+ cert.Dispose();
}
- else
+ // On non-macOS, outputCert is the round-tripped instance owned by the container;
+ // certWithKey is orphaned and should be disposed. On macOS, outputCert == certWithKey.
+ if (!ReferenceEquals(outputCert, certWithKey))
{
- outputCert = new X509Certificate2(container.Pfx, _password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
+ certWithKey.Dispose();
}
}
- container.Subject = subject;
- container.InternalCertificate = cert;
- container.Certificate = outputCert;
- container.Thumbprint = outputCert.Thumbprint;
-
Trace.WriteLine("[CertificateGenerator] generated a certificate:");
Trace.WriteLine(string.Format(" {0} = {1}", "isAuthority", isAuthority));
if (!isAuthority)
{
- Trace.WriteLine(string.Format(" {0} = {1}", "Signed by", signingCertificate.SubjectDN));
+ Trace.WriteLine(string.Format(" {0} = {1}", "Signed by", signingCertificate.SubjectName.Name));
Trace.WriteLine(string.Format(" {0} = {1}", "Subject (CN) ", subject));
Trace.WriteLine(string.Format(" {0} = {1}", "Subject Alt names ", string.Join(", ", subjectAlternativeNames)));
Trace.WriteLine(string.Format(" {0} = {1}", "Friendly Name ", certificateCreationSettings.FriendlyName));
@@ -514,97 +522,208 @@ private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMach
return container;
}
- private X509Crl CreateCrl(X509Certificate signingCertificate)
+ private byte[] ComputeSerialNumber(CertificateCreationSettings settings)
{
- EnsureInitialized();
+ // On non-Windows hosts, use a deterministic serial derived from FriendlyName so cleanup-by-serial works.
+ BigInteger serialBigInt;
+ if (!CertificateHelper.CurrentOperatingSystem.IsWindows() && settings != null && settings.FriendlyName != null)
+ {
+ serialBigInt = HashFriendlyName(settings.FriendlyName);
+ }
+ else
+ {
+ byte[] rand = new byte[8];
+ RandomNumberGenerator.Fill(rand);
+ rand[0] &= 0x7F;
+ serialBigInt = new BigInteger(rand, isUnsigned: true, isBigEndian: true);
+ }
- s_crlGenerator.Reset();
+ // CertificateRequest.Create(serialNumber) expects big-endian, minimum-length, unsigned.
+ return serialBigInt.ToByteArray(isUnsigned: true, isBigEndian: true);
+ }
- DateTime now = DateTime.UtcNow;
+ private static string SerialToHex(byte[] serialBigEndian)
+ {
+ string s = Convert.ToHexString(serialBigEndian).ToLowerInvariant().TrimStart('0');
+ return s.Length == 0 ? "0" : s;
+ }
+
+ private byte[] CreateCrl()
+ {
+ EnsureInitialized();
+ DateTime now = DateTime.UtcNow;
DateTime updateTime = now.Subtract(_crlValidityGracePeriodEnd);
- // Ensure that the update time for the CRL is no greater than the earliest time that the CA is valid for
- if (_defaultValidityNotBefore > now.Subtract(_crlValidityGracePeriodEnd))
+ if (_defaultValidityNotBefore > updateTime)
{
updateTime = _defaultValidityNotBefore;
}
+ DateTime nextUpdate = now.Add(_validityPeriod);
- s_crlGenerator.SetThisUpdate(updateTime);
- //There is no need to update CRL.
- s_crlGenerator.SetNextUpdate(now.Add(ValidityPeriod));
- s_crlGenerator.SetIssuerDN(signingCertificate.SubjectDN);
-
- s_crlGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(signingCertificate));
-
- BigInteger crlNumber = new BigInteger(64 /*bits for the number*/, _random).Abs();
- s_crlGenerator.AddExtension(X509Extensions.CrlNumber, false, new CrlNumber(crlNumber));
-
- foreach (var kvp in s_revokedCertificates)
+ CertificateRevocationListBuilder builder = new CertificateRevocationListBuilder();
+ foreach (KeyValuePair kvp in s_revokedCertificates)
{
- s_crlGenerator.AddCrlEntry(new BigInteger(kvp.Key, 16), kvp.Value, CrlReason.CessationOfOperation);
+ byte[] serial = HexToBytes(kvp.Key);
+ // AddEntry writes the bytes verbatim as the INTEGER content (signed encoding).
+ // Our stored serials are minimal-unsigned, so prepend a 0x00 sign byte when the
+ // high bit of the first byte is set, otherwise the value would be interpreted
+ // as negative and would not match the certificate's positively-encoded serial.
+ if (serial.Length > 0 && (serial[0] & 0x80) != 0)
+ {
+ byte[] padded = new byte[serial.Length + 1];
+ Buffer.BlockCopy(serial, 0, padded, 1, serial.Length);
+ serial = padded;
+ }
+ builder.AddEntry(serial, kvp.Value);
}
- ISignatureFactory signatureFactory = new Asn1SignatureFactory(_signatureAlgorithm, _authorityKeyPair.Private, _random);
- X509Crl crl = s_crlGenerator.Generate(signatureFactory);
- crl.Verify(_authorityKeyPair.Public);
+ byte[] crl = builder.Build(
+ issuerCertificate: _authorityCertWithKey,
+ crlNumber: GenerateCrlNumber(),
+ nextUpdate: nextUpdate,
+ hashAlgorithm: HashAlgorithmName.SHA256,
+ rsaSignaturePadding: RSASignaturePadding.Pkcs1,
+ thisUpdate: updateTime);
- Trace.WriteLine(string.Format("[CertificateGenerator] has created a Certificate Revocation List :"));
- Trace.WriteLine(string.Format(" {0} = {1}", "Issuer", crl.IssuerDN));
- Trace.WriteLine(string.Format(" {0} = {1}", "CRL Number", crlNumber));
+ Trace.WriteLine(string.Format("[CertificateGenerator] has created a Certificate Revocation List:"));
+ Trace.WriteLine(string.Format(" {0} = {1}", "Issuer", _authorityCertWithKey.SubjectName.Name));
+ Trace.WriteLine(string.Format(" {0} = {1} bytes", "Length", crl.Length));
return crl;
}
- // Throws an exception if the certificate is invalid
- private void EnsureCertificateIsValid(X509Certificate certificate)
+ private static BigInteger GenerateCrlNumber()
{
- certificate.CheckValidity(DateTime.UtcNow);
- certificate.Verify(_authorityKeyPair.Public);
+ byte[] rand = new byte[8];
+ RandomNumberGenerator.Fill(rand);
+ rand[0] &= 0x7F;
+ Array.Reverse(rand);
+ BigInteger n = new BigInteger(rand);
+ return n.Sign < 0 ? -n : n;
}
- private void EnsureInitialized()
+ private static X500DistinguishedName BuildDistinguishedName(string canonicalName)
{
- if (!_isInitialized)
+ // Order in DN: CN, O, OU (encoded inner-to-outer / RFC 4514 reverse of issuance)
+ // Use X500DistinguishedName parser; quote values that may contain spaces.
+ string dn = string.Format("CN={0}, O=DO_NOT_TRUST, OU=Created by https://github.com/dotnet/wcf",
+ EscapeDnComponent(canonicalName));
+ return new X500DistinguishedName(dn);
+ }
+
+ private static string EscapeDnComponent(string value)
+ {
+ // Minimal escaping for special chars in RFC 4514 DN strings.
+ StringBuilder sb = new StringBuilder(value.Length);
+ foreach (char c in value)
{
- Initialize();
+ if (c == ',' || c == '+' || c == '"' || c == '\\' || c == '<' || c == '>' || c == ';' || c == '=' || c == '#')
+ {
+ sb.Append('\\');
+ }
+ sb.Append(c);
}
+ return sb.ToString();
}
- private void EnsureNotInitialized(string paramName)
+ private static byte[] GetSubjectKeyIdentifierBytes(X509Certificate2 cert)
{
- if (_isInitialized)
+ foreach (X509Extension ext in cert.Extensions)
{
- throw new ArgumentException(paramName + " cannot be set as the generator has already been initialized.", paramName);
+ if (ext.Oid != null && ext.Oid.Value == Oids.SubjectKeyIdentifierExtension)
+ {
+ // SubjectKeyIdentifier extension; value is OCTET STRING containing OCTET STRING (the key id)
+ AsnReader r = new AsnReader(ext.RawData, AsnEncodingRules.DER);
+ return r.ReadOctetString();
+ }
+ }
+
+ // Fallback: compute SHA-1 of the DER-encoded SubjectPublicKey BIT STRING (just the key bytes).
+ using (SHA1 sha1 = SHA1.Create())
+ {
+ return sha1.ComputeHash(cert.PublicKey.EncodedKeyValue.RawData);
}
}
- private static X509Name CreateX509Name(string canonicalName)
+ // Builds CRLDistributionPoints extension for a single fullName URI distribution point.
+ // CRLDistributionPoints ::= SEQUENCE OF DistributionPoint
+ // DistributionPoint ::= SEQUENCE { distributionPoint [0] EXPLICIT DistributionPointName OPTIONAL, ... }
+ // DistributionPointName ::= CHOICE { fullName [0] IMPLICIT GeneralNames, ... }
+ // GeneralName ::= CHOICE { uniformResourceIdentifier [6] IMPLICIT IA5String, ... }
+ private static X509Extension BuildCrlDistributionPointsExtension(string url)
{
- X509Name authorityX509Name;
+ AsnWriter w = new AsnWriter(AsnEncodingRules.DER);
+ using (w.PushSequence())
+ {
+ using (w.PushSequence())
+ {
+ using (w.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true)))
+ {
+ using (w.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true)))
+ {
+ w.WriteCharacterString(UniversalTagNumber.IA5String, url, new Asn1Tag(TagClass.ContextSpecific, 6, isConstructed: false));
+ }
+ }
+ }
+ }
+ return new X509Extension(Oids.CrlDistributionPointsExtensionOid, w.Encode(), critical: false);
+ }
+
- IList authorityKeyIdOrder = new List();
- IDictionary authorityKeyIdName = new Dictionary();
+ private static byte[] HexToBytes(string hex)
+ {
+ if (string.IsNullOrEmpty(hex))
+ {
+ return Array.Empty();
+ }
+ string s = (hex.Length & 1) == 1 ? "0" + hex : hex;
+ return Convert.FromHexString(s);
+ }
- authorityKeyIdOrder.Add(X509Name.OU);
- authorityKeyIdOrder.Add(X509Name.O);
- authorityKeyIdOrder.Add(X509Name.CN);
+ private static BigInteger HexToBigInteger(string hex)
+ {
+ if (string.IsNullOrEmpty(hex))
+ {
+ return BigInteger.Zero;
+ }
+ // Ensure positive parsing
+ string s = hex.StartsWith("0", StringComparison.Ordinal) ? hex : "0" + hex;
+ return BigInteger.Parse(s, System.Globalization.NumberStyles.HexNumber);
+ }
- authorityKeyIdName.Add(X509Name.CN, canonicalName);
- authorityKeyIdName.Add(X509Name.O, "DO_NOT_TRUST");
- authorityKeyIdName.Add(X509Name.OU, "Created by https://github.com/dotnet/wcf");
+ // Throws an exception if the certificate is invalid.
+ private void EnsureCertificateIsValid(X509Certificate2 certificate)
+ {
+ DateTime now = DateTime.UtcNow;
+ if (now < certificate.NotBefore.ToUniversalTime() || now > certificate.NotAfter.ToUniversalTime())
+ {
+ throw new CryptographicException(string.Format("Certificate is outside its validity window: {0} - {1}", certificate.NotBefore, certificate.NotAfter));
+ }
+ }
- authorityX509Name = new X509Name(authorityKeyIdOrder, authorityKeyIdName);
+ private void EnsureInitialized()
+ {
+ if (!_isInitialized)
+ {
+ Initialize();
+ }
+ }
- return authorityX509Name;
+ private void EnsureNotInitialized(string paramName)
+ {
+ if (_isInitialized)
+ {
+ throw new ArgumentException(paramName + " cannot be set as the generator has already been initialized.", paramName);
+ }
}
public bool RevokeCertificateBySerialNumber(string serialNum)
{
bool success = false;
- BigInteger serialNumBigInt = null;
try
{
- serialNumBigInt = new BigInteger(str: serialNum, radix: 16);
+ // Validate hex parses
+ HexToBigInteger(serialNum);
success = true;
}
catch (FormatException)
@@ -618,8 +737,8 @@ public bool RevokeCertificateBySerialNumber(string serialNum)
s_revokedCertificates.Add(serialNum, DateTime.UtcNow);
}
- // Note that we don't actually check against the thumbprints here, we just go ahead and stick the serial
- // number into the CRL without checking whether or not we've ever generated it
+ // Note that we don't actually check against the thumbprints here, we just go ahead and stick the serial
+ // number into the CRL without checking whether or not we've ever generated it.
Trace.WriteLine(string.Format("[CertificateGenerator] Revoke certificate with serial number {0}: ", success ? "succeeded" : "FAILED"));
return success;
}
@@ -628,9 +747,8 @@ public bool RevokeCertificateBySerialNumber(string serialNum)
public class X509CertificateContainer
{
public string Subject { get; internal set; }
- internal X509Certificate InternalCertificate { get; set; }
public X509Certificate2 Certificate { get; internal set; }
- internal byte[] Pfx { get; set; }
+ public byte[] Pfx { get; internal set; }
public string Thumbprint { get; internal set; }
}
}
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.cs b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.cs
index 09d1b13e7e1..fd1791dacf5 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.cs
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.cs
@@ -22,26 +22,36 @@ public class CertificateGeneratorLibrary
private static void RemoveCertificatesFromStore(StoreName storeName, StoreLocation storeLocation)
{
+ // On macOS, all cert operations go through a custom keychain managed by CertificateHelper.
+ // Cleanup is handled by deleting the entire keychain in UninstallAllCerts.
+ if (CertificateHelper.CurrentOperatingSystem.IsMacOS())
+ {
+ return;
+ }
+
X509Store store = CertificateHelper.GetX509Store(storeName, storeLocation);
Console.WriteLine(" Checking StoreName '{0}', StoreLocation '{1}'", storeName, store.Location);
+ store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
+ foreach (var cert in store.Certificates.Find(X509FindType.FindByIssuerName, CertificateIssuer, false))
{
- if (!CertificateHelper.CurrentOperatingSystem.IsMacOS())
- {
- store.Open(OpenFlags.ReadWrite | OpenFlags.IncludeArchived);
- }
-
- foreach (var cert in store.Certificates.Find(X509FindType.FindByIssuerName, CertificateIssuer, false))
- {
- Console.Write(" {0}. Subject: '{1}'", cert.Thumbprint, cert.SubjectName.Name);
- store.Remove(cert);
- Console.WriteLine(" ... removed");
- }
+ Console.Write(" {0}. Subject: '{1}'", cert.Thumbprint, cert.SubjectName.Name);
+ store.Remove(cert);
+ Console.WriteLine(" ... removed");
}
Console.WriteLine();
}
public static void UninstallAllCerts()
{
+ // On macOS, delete the custom keychain which removes all WCF test certs at once
+ if (CertificateHelper.CurrentOperatingSystem.IsMacOS())
+ {
+ Console.WriteLine(" Cleaning up macOS keychain...");
+ CertificateHelper.DeleteMacOSKeychain();
+ Console.WriteLine();
+ return;
+ }
+
RemoveCertificatesFromStore(StoreName.My, StoreLocation.CurrentUser);
RemoveCertificatesFromStore(StoreName.My, StoreLocation.LocalMachine);
@@ -147,7 +157,8 @@ public static int SetupCerts(string testserverbase, TimeSpan validatePeriod, str
ValidityType = CertificateValidityType.Valid,
Subject = s_fqdn,
SubjectAlternativeNames = new string[] { s_fqdn, s_hostname, "localhost" },
- EKU = new List { Org.BouncyCastle.Asn1.X509.KeyPurposeID.id_kp_clientAuth }
+ // Only clientAuth EKU - intentionally missing serverAuth to exercise invalid-EKU scenario.
+ EKU = new List { Oids.ClientAuthEku }
};
CreateAndInstallMachineCertificate(certificateGenerate, certificateCreationSettings);
@@ -157,7 +168,7 @@ public static int SetupCerts(string testserverbase, TimeSpan validatePeriod, str
FriendlyName = "WCF Bridge - STSMetaData",
ValidityType = CertificateValidityType.Valid,
Subject = "STSMetaData",
- EKU = new List()
+ EKU = new List()
};
CreateAndInstallMachineCertificate(certificateGenerate, certificateCreationSettings);
@@ -167,8 +178,8 @@ public static int SetupCerts(string testserverbase, TimeSpan validatePeriod, str
FriendlyName = "WCF Bridge - UserCertificateResource",
Subject = "WCF Client Certificate",
};
- X509Certificate2 certificate = certificateGenerate.CreateUserCertificate(certificateCreationSettings).Certificate;
- CertificateManager.AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, certificate);
+ var userCertContainer = certificateGenerate.CreateUserCertificate(certificateCreationSettings);
+ CertificateManager.AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, userCertContainer.Certificate);
//Create CRL and save it
FileInfo file = new FileInfo(s_crlFileLocation);
@@ -181,7 +192,7 @@ public static int SetupCerts(string testserverbase, TimeSpan validatePeriod, str
private static void CreateAndInstallMachineCertificate(CertificateGenerator certificateGenerate, CertificateCreationSettings certificateCreationSettings)
{
- X509Certificate2 certificate = certificateGenerate.CreateMachineCertificate(certificateCreationSettings).Certificate;
- CertificateManager.AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, certificate);
+ var container = certificateGenerate.CreateMachineCertificate(certificateCreationSettings);
+ CertificateManager.AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, container.Certificate);
}
}
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.csproj b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.csproj
index d539f59a920..312c40b2de5 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.csproj
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateGeneratorLibrary.csproj
@@ -1,16 +1,11 @@
- netstandard2.0
+ net10.0
-
-
-
-
-
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateManager.cs b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateManager.cs
index e3fc2c15089..dbeb67fefd1 100644
--- a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateManager.cs
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/CertificateManager.cs
@@ -44,17 +44,50 @@ private static X509Certificate2 CertificateFromThumbprint(X509Store store, strin
// already present. Returns 'true' if the certificate was added.
public static bool AddToStoreIfNeeded(StoreName storeName, StoreLocation storeLocation, X509Certificate2 certificate)
{
+ if (certificate == null)
+ {
+ Trace.WriteLine("[CertificateManager] AddToStoreIfNeeded called with null certificate.");
+ return false;
+ }
+
+ // On macOS, the X509Store API cannot modify Root/TrustedPeople stores without user interaction,
+ // and the login keychain may be locked in CI. Use the macOS security CLI with a custom unlocked
+ // keychain instead. Route all stores into the custom keychain (macOS doesn't have proper
+ // per-store separation anyway — see CertificateHelper.GetX509Store).
+ if (CertificateHelper.CurrentOperatingSystem.IsMacOS())
+ {
+ // Root certs need OS-level trust (admin domain) to be honored by SecTrust during TLS
+ // handshakes. Do NOT also import them into the custom keychain - it sits first in the
+ // user search list and would shadow the trusted System.keychain entry, causing
+ // SecTrust to report UntrustedRoot at TLS time.
+ if (storeName == StoreName.Root)
+ {
+ return CertificateHelper.AddTrustedCertOnMacOS(certificate);
+ }
+
+ // Always import the PFX (with private key) when the cert has one — services hosting the
+ // cert (e.g., a TrustedPeople peer cert used by SSL) need the private key, not just the
+ // public bytes.
+ bool imported;
+ if (certificate.HasPrivateKey)
+ {
+ byte[] pfxBytes = certificate.Export(X509ContentType.Pkcs12, "test");
+ imported = CertificateHelper.ImportCertToMacOSKeychain(pfxBytes, "test");
+ }
+ else
+ {
+ imported = CertificateHelper.ImportPublicCertToMacOSKeychain(certificate);
+ }
+
+ return imported;
+ }
+
X509Store store = null;
X509Certificate2 existingCert = null;
try
{
store = CertificateHelper.GetX509Store(storeName, storeLocation);
-
- // We assume Bridge is running elevated
- if (!CertificateHelper.CurrentOperatingSystem.IsMacOS())
- {
- store.Open(OpenFlags.ReadWrite);
- }
+ store.Open(OpenFlags.ReadWrite);
existingCert = CertificateFromThumbprint(store, certificate.Thumbprint);
if (existingCert == null)
{
@@ -109,9 +142,8 @@ public static string InstallCertificateToMyStore(X509Certificate2 certificate, b
{
lock (s_certificateLock)
{
- bool added = AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, certificate);
-
- return certificate.Thumbprint;
+ AddToStoreIfNeeded(StoreName.My, StoreLocation.LocalMachine, certificate);
+ return certificate?.Thumbprint;
}
}
@@ -122,9 +154,8 @@ public static string InstallCertificateToTrustedPeopleStore(X509Certificate2 cer
{
lock (s_certificateLock)
{
- bool added = AddToStoreIfNeeded(StoreName.TrustedPeople, StoreLocation.LocalMachine, certificate);
-
- return certificate.Thumbprint;
+ AddToStoreIfNeeded(StoreName.TrustedPeople, StoreLocation.LocalMachine, certificate);
+ return certificate?.Thumbprint;
}
}
@@ -162,7 +193,8 @@ public static X509Certificate2 CreateAndInstallLocalMachineCertificates(Certific
Subject = fqdn,
SubjectAlternativeNames = new string[] { fqdn, hostname, "localhost" }
};
- var hostCert = certificateGenerator.CreateMachineCertificate(certificateCreationSettings).Certificate;
+ var hostCertContainer = certificateGenerator.CreateMachineCertificate(certificateCreationSettings);
+ var hostCert = hostCertContainer.Certificate;
// Since s_myCertificates keys by subject name, we won't install a cert for the same subject twice
// only the first-created cert will win
@@ -178,7 +210,8 @@ public static X509Certificate2 CreateAndInstallLocalMachineCertificates(Certific
Subject = fqdn,
SubjectAlternativeNames = new string[] { fqdn, hostname, "localhost" }
};
- var peerCert = certificateGenerator.CreateMachineCertificate(certificateCreationSettings).Certificate;
+ var peerCertContainer = certificateGenerator.CreateMachineCertificate(certificateCreationSettings);
+ var peerCert = peerCertContainer.Certificate;
InstallCertificateToTrustedPeopleStore(peerCert, certificateCreationSettings.ValidityType == CertificateValidityType.Valid);
}
diff --git a/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/Oids.cs b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/Oids.cs
new file mode 100644
index 00000000000..071ac3c961d
--- /dev/null
+++ b/src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGeneratorLibrary/Oids.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Security.Cryptography;
+
+namespace WcfTestCommon
+{
+ // Centralized OID constants used throughout the CertificateGenerator project.
+ // Keep raw OID strings in one place so callers reference these instead of
+ // sprinkling numeric literals across the codebase.
+ internal static class Oids
+ {
+ // Extended Key Usage OIDs (RFC 5280)
+ public const string ServerAuthEku = "1.3.6.1.5.5.7.3.1";
+ public const string ClientAuthEku = "1.3.6.1.5.5.7.3.2";
+
+ // X.509 v3 extension OIDs (RFC 5280)
+ public const string SubjectKeyIdentifierExtension = "2.5.29.14";
+ public const string CrlDistributionPointsExtension = "2.5.29.31";
+ public const string AuthorityInfoAccessExtension = "1.3.6.1.5.5.7.1.1";
+
+ // AuthorityInfoAccess accessMethod OIDs (RFC 5280 4.2.2.1)
+ public const string IdAdCaIssuers = "1.3.6.1.5.5.7.48.2";
+
+ // Friendly names used when surfacing OIDs in cert viewers.
+ public const string ServerAuthEkuFriendlyName = "TLS Web Server Authentication";
+ public const string ClientAuthEkuFriendlyName = "TLS Web Client Authentication";
+ public const string CrlDistributionPointsExtensionFriendlyName = "X509v3 CRL Distribution Points";
+ public const string AuthorityInfoAccessExtensionFriendlyName = "Authority Information Access";
+
+ // Strongly-typed Oid instances (include friendly names that show up in tools
+ // that surface Oid.FriendlyName, e.g. certificate viewers).
+ public static readonly Oid ServerAuthEkuOid = new Oid(ServerAuthEku, ServerAuthEkuFriendlyName);
+ public static readonly Oid ClientAuthEkuOid = new Oid(ClientAuthEku, ClientAuthEkuFriendlyName);
+ public static readonly Oid CrlDistributionPointsExtensionOid = new Oid(CrlDistributionPointsExtension, CrlDistributionPointsExtensionFriendlyName);
+ public static readonly Oid AuthorityInfoAccessExtensionOid = new Oid(AuthorityInfoAccessExtension, AuthorityInfoAccessExtensionFriendlyName);
+ }
+}
diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/CertificateHelper/CertificateHelper.cs b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/CertificateHelper/CertificateHelper.cs
index 5d07500ab9e..2e788b0cb18 100644
--- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/CertificateHelper/CertificateHelper.cs
+++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/CertificateHelper/CertificateHelper.cs
@@ -2,16 +2,19 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
+using System.Diagnostics;
using System.IO;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
-using Infrastructure.Common;
namespace WcfTestCommon
{
public static class CertificateHelper
{
+ private static readonly string s_macOSKeychainPath = Path.Combine(Environment.CurrentDirectory, "wcf-test.keychain-db");
+ private const string MacOSKeychainPassword = "WcfTestPassword";
+ private static bool s_macOSKeychainInitialized;
+
public static class CurrentOperatingSystem
{
///
@@ -41,7 +44,7 @@ public static bool IsMacOS()
///
/// Returns true if current OS matches OSPlatform
///
- /// OS Platform to check for
+ /// OS Platform to check for
public static bool IsOSPlatform(OSPlatform osPlatform)
{
return RuntimeInformation.IsOSPlatform(osPlatform);
@@ -50,77 +53,261 @@ public static bool IsOSPlatform(OSPlatform osPlatform)
public static X509Store GetX509Store(StoreName storeName, StoreLocation storeLocation)
{
- X509Store store = null;
+ X509Store store;
if (CurrentOperatingSystem.IsWindows())
{
store = new X509Store(storeName, storeLocation);
}
- else if (CurrentOperatingSystem.IsLinux())
+ else if (CurrentOperatingSystem.IsMacOS())
{
- // Store the certificates in CurrentUser Scope
- store = new X509Store(storeName, StoreLocation.CurrentUser);
+ // macOS doesn't have proper per-store separation. .NET's X509Store(TrustedPeople|Root, CurrentUser)
+ // on macOS does not enumerate certs imported into the user's default keychain via the
+ // 'security' CLI. Route all store names through StoreName.My so adds and lookups land in
+ // the same place — the user's default keychain (which is our custom WCF test keychain).
+ store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
}
- else if (CurrentOperatingSystem.IsMacOS())
+ else
{
- // MacOS SafeKeychainHandle
- store = GetMacOSX509Store();
+ // On Linux, use CurrentUser scope as LocalMachine is not supported.
+ store = new X509Store(storeName, StoreLocation.CurrentUser);
}
- store = EnsureStoreIsOpened(store);
+ store.Open(OpenFlags.ReadOnly);
return store;
}
- private static X509Store EnsureStoreIsOpened(X509Store store)
+ ///
+ /// Ensures a custom unlocked keychain exists on macOS for non-interactive cert operations.
+ /// The login keychain requires user interaction; this custom keychain does not.
+ ///
+ public static void EnsureMacOSKeychainInitialized()
{
- try
+ if (s_macOSKeychainInitialized)
{
- // Try opening the store in read-only mode
- store.Open(OpenFlags.ReadOnly);
+ return;
}
- catch { }
- return store;
+ if (File.Exists(s_macOSKeychainPath))
+ {
+ RunSecurityCommand(string.Format("delete-keychain \"{0}\"", s_macOSKeychainPath));
+ }
+
+ // Create a new unlocked keychain dedicated to WCF test certificates
+ RunSecurityCommand(string.Format("create-keychain -p {0} \"{1}\"", MacOSKeychainPassword, s_macOSKeychainPath));
+ RunSecurityCommand(string.Format("unlock-keychain -p {0} \"{1}\"", MacOSKeychainPassword, s_macOSKeychainPath));
+ // Disable auto-lock so the keychain stays unlocked for the duration of the test run
+ RunSecurityCommand(string.Format("set-keychain-settings \"{0}\"", s_macOSKeychainPath));
+
+ // Add the custom keychain to the search list so certs are discoverable
+ string existingKeychains = RunSecurityCommand("list-keychains -d user").Trim().Replace("\"", "");
+ RunSecurityCommand(string.Format("list-keychains -d user -s \"{0}\" {1}", s_macOSKeychainPath, existingKeychains));
+
+ // Set as default keychain so .NET's X509Store(My, CurrentUser) searches it
+ RunSecurityCommand(string.Format("default-keychain -s \"{0}\"", s_macOSKeychainPath));
+
+ s_macOSKeychainInitialized = true;
+ Trace.WriteLine(string.Format("[CertificateHelper] macOS keychain initialized at: {0}", s_macOSKeychainPath));
}
- internal static string OSXCustomKeychainFilePath
+ ///
+ /// Imports a PFX (PKCS12) file into the macOS custom keychain
+ /// using the 'security import' CLI, avoiding user interaction prompts.
+ ///
+ public static bool ImportCertToMacOSKeychain(byte[] pfxBytes, string pfxPassword)
{
- get
+ EnsureMacOSKeychainInitialized();
+
+ string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".pfx");
+ try
+ {
+ File.WriteAllBytes(tempFile, pfxBytes);
+
+ // -A allows any application to access the imported key without prompting
+ string output = RunSecurityCommand(string.Format(
+ "import \"{0}\" -k \"{1}\" -P \"{2}\" -A -T /usr/bin/security",
+ tempFile, s_macOSKeychainPath, pfxPassword));
+
+ Trace.WriteLine("[CertificateHelper] Imported PFX to macOS keychain.");
+
+ return true;
+ }
+ finally
{
- return Path.Combine(Environment.CurrentDirectory, "wcfLocal.keychain");
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
}
}
- internal static string OSXCustomKeychainPassword
+ ///
+ /// Imports a public certificate (no private key) into the macOS custom keychain.
+ ///
+ public static bool ImportPublicCertToMacOSKeychain(X509Certificate2 certificate)
{
- get
+ EnsureMacOSKeychainInitialized();
+
+ string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".cer");
+ try
+ {
+ File.WriteAllBytes(tempFile, certificate.Export(X509ContentType.Cert));
+ RunSecurityCommand(string.Format(
+ "import \"{0}\" -k \"{1}\" -A -T /usr/bin/security",
+ tempFile, s_macOSKeychainPath));
+
+ Trace.WriteLine(string.Format("[CertificateHelper] Imported public certificate to macOS keychain:"));
+ Trace.WriteLine(string.Format(" {0} = {1}", "CN", certificate.SubjectName.Name));
+ Trace.WriteLine(string.Format(" {0} = {1}", "Thumbprint", certificate.Thumbprint));
+ return true;
+ }
+ finally
{
- return "WCFKeychainFilePassword";
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
}
}
- [MethodImpl(MethodImplOptions.NoInlining)]
- public static X509Store GetMacOSX509Store(string storeFilePath = null)
+ ///
+ /// On macOS, add a certificate as a trusted root using the 'security' CLI.
+ ///
+ public static bool AddTrustedCertOnMacOS(X509Certificate2 certificate)
+ {
+ return AddTrustedCertOnMacOS(certificate.Export(X509ContentType.Cert));
+ }
+
+ ///
+ /// Adds trust for a certificate (from raw DER bytes) on macOS using the 'security' CLI.
+ /// Writes trust settings into the admin trust domain (System.keychain) via sudo so that
+ /// macOS's TLS chain evaluator honors the root system-wide. Helix macOS runners are
+ /// configured with passwordless sudo, so this is non-interactive.
+ ///
+ public static bool AddTrustedCertOnMacOS(byte[] certDerBytes)
{
- if (storeFilePath == null)
+ string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".cer");
+ try
+ {
+ File.WriteAllBytes(tempFile, certDerBytes);
+
+ // sudo -n: non-interactive; fail rather than prompt.
+ // -d: admin trust domain (system-wide), requires root.
+ // -r trustRoot: this cert is a trust root.
+ // NO -p flag: empty trust settings array means "trusted for all uses" - broader
+ // than "-p ssl" which constrains trust to the sslServer policy only. Without -p
+ // SecTrust treats the root as a universal trust anchor.
+ // -k System.keychain: store the cert in the system keychain.
+ // add-trusted-cert -d writes to the admin trust domain via SecTrustSettings, which
+ // on headless macOS CI runners intermittently fails with "SecTrustSettingsSet-
+ // TrustSettings: The authorization was denied since no user interaction was
+ // possible." The failure is transient, so retry a few times with a short backoff.
+ string addTrustedArgs = string.Format(
+ "-n security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \"{0}\"",
+ tempFile);
+
+ const int maxAttempts = 5;
+ for (int attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ string stdout, stderr;
+ int exitCode = RunProcess("sudo", addTrustedArgs, 30000, out stdout, out stderr);
+ if (exitCode == 0)
+ {
+ Console.WriteLine("[CertificateHelper] Added root cert to macOS System.keychain admin trust domain.");
+ SettleMacOSTrust(tempFile);
+ return true;
+ }
+
+ Console.Error.WriteLine(string.Format(
+ "[CertificateHelper] sudo security add-trusted-cert failed (attempt {0}/{1}, exit {2}): {3}",
+ attempt, maxAttempts, exitCode, stderr));
+
+ if (attempt < maxAttempts)
+ {
+ System.Threading.Thread.Sleep(2000);
+ }
+ }
+
+ return false;
+ }
+ finally
{
- storeFilePath = OSXCustomKeychainFilePath;
+ if (File.Exists(tempFile))
+ {
+ File.Delete(tempFile);
+ }
}
+ }
- SafeKeychainHandle keychain;
- if (!File.Exists(storeFilePath))
+ ///
+ /// Deletes the custom macOS keychain used for WCF test certificates.
+ ///
+ public static void DeleteMacOSKeychain()
+ {
+ if (File.Exists(s_macOSKeychainPath))
{
- keychain = SafeKeychainHandle.Create(storeFilePath, OSXCustomKeychainPassword);
+ RunSecurityCommand(string.Format("delete-keychain \"{0}\"", s_macOSKeychainPath));
+ s_macOSKeychainInitialized = false;
+ Trace.WriteLine("[CertificateHelper] macOS keychain deleted.");
}
- else
+ }
+
+ ///
+ /// Blocks until trustd has committed the admin trust domain written by
+ /// add-trusted-cert. Running synchronous `security` operations that consult the
+ /// trust store forces trustd to finish reloading before we proceed, which prevents
+ /// the first macOS X509Chain build from racing and reporting UntrustedRoot. This is a
+ /// best-effort barrier, so command results are intentionally ignored.
+ ///
+ private static void SettleMacOSTrust(string rootCertFile)
+ {
+ string stdout, stderr;
+ RunProcess("security", "trust-settings-export -d /tmp/wcf-trust-admin.plist", 15000, out stdout, out stderr);
+ RunProcess("security", "verify-cert -c \"" + rootCertFile + "\" -p ssl", 15000, out stdout, out stderr);
+ }
+
+ // Runs a `security` subcommand, logs a diagnostic on non-zero exit, and returns its stdout.
+ private static string RunSecurityCommand(string arguments)
+ {
+ string stdout, stderr;
+ int exitCode = RunProcess("security", arguments, 30000, out stdout, out stderr);
+ if (exitCode != 0)
{
- keychain = SafeKeychainHandle.Open(storeFilePath, OSXCustomKeychainPassword);
+ Console.Error.WriteLine(string.Format("[CertificateHelper] security {0} failed (exit code {1}): {2}",
+ arguments, exitCode, stderr));
}
- if (keychain.IsInvalid)
- throw new Exception("Unable to open MacOS Keychain");
+ return stdout;
+ }
- X509Store store = new X509Store(keychain.DangerousGetHandle());
- return store;
+ // Core process runner for all `security` / `sudo` invocations. Captures stdout/stderr and
+ // returns the process exit code, or -1 if the process could not be started or timed out.
+ private static int RunProcess(string fileName, string arguments, int timeoutMilliseconds, out string stdout, out string stderr)
+ {
+ stdout = string.Empty;
+ stderr = string.Empty;
+ try
+ {
+ var psi = new ProcessStartInfo
+ {
+ FileName = fileName,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ };
+ using (var process = Process.Start(psi))
+ {
+ stdout = process.StandardOutput.ReadToEnd();
+ stderr = process.StandardError.ReadToEnd();
+ process.WaitForExit(timeoutMilliseconds);
+ return process.ExitCode;
+ }
+ }
+ catch (Exception ex)
+ {
+ stderr = ex.Message;
+ return -1;
+ }
}
}
}
diff --git a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/TestHost.cs b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/TestHost.cs
index 28a8d45d7ca..82dea84e5e1 100644
--- a/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/TestHost.cs
+++ b/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/TestHost.cs
@@ -224,7 +224,12 @@ public static X509Certificate2 CertificateFromSubject(StoreName name, StoreLocat
{
store = CertificateHelper.GetX509Store(name, location);
- X509Certificate2Collection foundCertificates = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, validOnly: true);
+ // validOnly: false — on macOS, our self-signed root in the custom keychain is not
+ // necessarily recognized by the OS chain builder, so requiring a fully-valid chain
+ // would filter the root out and cause /TestHost.svc/RootCert to return 500. The
+ // caller is responsible for any additional validation it needs (e.g., the test
+ // client installs whatever root it gets back).
+ X509Certificate2Collection foundCertificates = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName, validOnly: false);
return foundCertificates.Count == 0 ? null : foundCertificates[0];
}
finally
@@ -259,6 +264,7 @@ public static X509Certificate2 CertificateFromFriendlyName(StoreName name, Store
return cert;
}
}
+
return null;
}
finally
diff --git a/src/System.Private.ServiceModel/tools/scripts/CleanUpWCFSelfHostedSvc.cmd b/src/System.Private.ServiceModel/tools/scripts/CleanUpWCFSelfHostedSvc.cmd
index 4c443cd6e8b..e53497196c4 100644
--- a/src/System.Private.ServiceModel/tools/scripts/CleanUpWCFSelfHostedSvc.cmd
+++ b/src/System.Private.ServiceModel/tools/scripts/CleanUpWCFSelfHostedSvc.cmd
@@ -20,7 +20,7 @@ REM The CMD we call self-elevates and logs its results to %_cleanuplog%
REM Errors stopping the service are logged but do not stop processing
call %~dp0StopWcfSelfHostedSvc.cmd
-If NOT exist %~dp0..\..\..\..\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe (
+If NOT exist %~dp0..\..\..\..\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe (
echo Building certificate generator...
call %~dp0BuildCertUtil.cmd >>%_cleanuplog%
set __EXITCODE=%ERRORLEVEL%
@@ -55,9 +55,9 @@ if NOT [%ERRORLEVEL%]==[0] (
echo Removing certificates. >>%_cleanuplog%
if '%_runelevated%' == '' (
- call %~dp0..\..\..\..\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe -Uninstall >>%_cleanuplog%
+ call %~dp0..\..\..\..\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe -Uninstall >>%_cleanuplog%
) else (
- call %_runelevated% %~dp0..\..\..\..\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe -Uninstall >nul
+ call %_runelevated% %~dp0..\..\..\..\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe -Uninstall >nul
)
if NOT [%ERRORLEVEL%]==[0] (
diff --git a/src/System.Private.ServiceModel/tools/scripts/InstallRootCertificate.sh b/src/System.Private.ServiceModel/tools/scripts/InstallRootCertificate.sh
index fae4b6d8ff5..123d44a49a8 100755
--- a/src/System.Private.ServiceModel/tools/scripts/InstallRootCertificate.sh
+++ b/src/System.Private.ServiceModel/tools/scripts/InstallRootCertificate.sh
@@ -38,8 +38,36 @@ install_root_cert()
case ${__os} in
"darwin")
- # OS X SecureTransport does a direct install into the cert store without requiring copying into a location
- $__update_os_certbundle_exec -v add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${__cafile}
+ # macOS: install into the System keychain (admin domain) and grant full trust.
+ # `add-trusted-cert -d` targets the admin domain (System.keychain). It is
+ # non-interactive only when invoked as root, which is always the case here
+ # (helix runs this script under `sudo -E`). We deliberately omit `-p ssl`
+ # so the cert is trusted for ALL policies; specifying a policy narrows trust
+ # to that policy only and has produced inconsistent SslStream chain validation
+ # results in CI (dotnet/wcf#2870).
+ #
+ # add-trusted-cert -d writes to the admin trust domain via SecTrustSettings, which on
+ # headless macOS CI runners intermittently fails with "SecTrustSettingsSetTrustSettings:
+ # The authorization was denied since no user interaction was possible." The failure is
+ # transient, so retry a few times with a short backoff.
+ __attempt=1
+ __max_attempts=5
+ while [ ${__attempt} -le ${__max_attempts} ]; do
+ $__update_os_certbundle_exec -v add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${__cafile}
+ __add_rc=$?
+ if [ ${__add_rc} -eq 0 ]; then
+ break
+ fi
+ echo "[InstallRootCertificate] add-trusted-cert attempt ${__attempt}/${__max_attempts} failed (exit ${__add_rc}); retrying after 2s..."
+ __attempt=$((__attempt + 1))
+ sleep 2
+ done
+
+ # Force trustd to drop its in-memory cache and re-read the trust settings so
+ # the newly-installed root takes effect immediately for SecTrustEvaluate
+ # (used by .NET's X509Chain on macOS); otherwise it can keep returning the
+ # previous "untrusted" verdict for the lifetime of the helix VM.
+ killall -HUP trustd 2>/dev/null || true
;;
"centos" | "rhel" | "fedora")
cp -f "${__cafile}" /etc/pki/ca-trust/source/anchors
@@ -141,7 +169,7 @@ fi
# OpenSSL rehash - applicable on all platforms
-__c_rehash_exec=`which c_rehash`
+__c_rehash_exec=`command -v c_rehash`
if [ $? -ne 0 -o ! -f "$__c_rehash_exec" ]; then
echo "WARNING: Could not find 'c_rehash'. Is OpenSSL installed properly?"
fi
@@ -163,13 +191,13 @@ case ${__os} in
;;
esac
-__update_os_certbundle_exec=`which ${__update_os_certbundle_cmd}`
+__update_os_certbundle_exec=`command -v ${__update_os_certbundle_cmd}`
if [ $? -ne 0 -o ! -f "$__update_os_certbundle_exec" ]; then
echo "ERROR: Could not find '${__update_os_certbundle_cmd}', which is needed to update certificates on '${__os}'"
exit 1
fi
-__curl_exe=`which curl`
+__curl_exe=`command -v curl`
if [ ! -e "$__curl_exe" ]; then
echo "Could not find cURL"
diff --git a/src/System.Private.ServiceModel/tools/scripts/RefreshServerCertificates.cmd b/src/System.Private.ServiceModel/tools/scripts/RefreshServerCertificates.cmd
index 64b1fb3fcfb..f0b1c849110 100644
--- a/src/System.Private.ServiceModel/tools/scripts/RefreshServerCertificates.cmd
+++ b/src/System.Private.ServiceModel/tools/scripts/RefreshServerCertificates.cmd
@@ -38,8 +38,8 @@ TASKKILL /F /IM SelfHostedWCFService.exe
echo call %_SCRIPTSDIR%\BuildCertUtil.cmd >> %_LOGFILE%
call %_SCRIPTSDIR%\BuildCertUtil.cmd >> %_LOGFILE%
-echo [%~n0] cmd /c %_GITREPO%\artifacts\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe >> %_LOGFILE%
-cmd /c %_GITREPO%\artifacts\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe >> %_LOGFILE% 2>&1
+echo [%~n0] cmd /c %_GITREPO%\artifacts\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe >> %_LOGFILE%
+cmd /c %_GITREPO%\artifacts\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe >> %_LOGFILE% 2>&1
if NOT "%ERRORLEVEL%"=="0" (
echo Warning: An error occurred when calling CertificateGenerator.exe. >> %_LOGFILE%
diff --git a/src/System.Private.ServiceModel/tools/scripts/SetupWcfIISHostedService.cmd b/src/System.Private.ServiceModel/tools/scripts/SetupWcfIISHostedService.cmd
index 0573c5c8a12..257555fe5d4 100644
--- a/src/System.Private.ServiceModel/tools/scripts/SetupWcfIISHostedService.cmd
+++ b/src/System.Private.ServiceModel/tools/scripts/SetupWcfIISHostedService.cmd
@@ -192,7 +192,7 @@ if ERRORLEVEL 1 goto :Failure
echo Run CertificateGenerator tool. This will take a little while...
md %_wcfTestDir%
-set certGen=%_certRepo%\artifacts\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe
+set certGen=%_certRepo%\artifacts\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe
echo ^^^^^^^^^^^>%certGen%.config
call :Run %certGen%
if ERRORLEVEL 1 goto :Failure
diff --git a/src/System.Private.ServiceModel/tools/scripts/StartWCFSelfHostedSvcDoWork.cmd b/src/System.Private.ServiceModel/tools/scripts/StartWCFSelfHostedSvcDoWork.cmd
index ac4fad76b5f..ca1923be130 100644
--- a/src/System.Private.ServiceModel/tools/scripts/StartWCFSelfHostedSvcDoWork.cmd
+++ b/src/System.Private.ServiceModel/tools/scripts/StartWCFSelfHostedSvcDoWork.cmd
@@ -35,7 +35,7 @@ REM we need the direcotry to save the test.crl file. We are investigate a way to
md c:\wcftest
REM Certificate configuration errors are all non fatal currently because we non cert tests will still pass
echo Generating certificates ...
-%~dp0..\..\..\..\artifacts\bin\CertificateGenerator\Release\net471\CertificateGenerator.exe >>%_setuplog%
+%~dp0..\..\..\..\artifacts\bin\CertificateGenerator\Release\net10.0\CertificateGenerator.exe >>%_setuplog%
if NOT [%ERRORLEVEL%]==[0] (
echo Warning: An error occurred while running certificate generator. >>%_setuplog%
)