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% )