diff --git a/public/Set-DbaNetworkCertificate.ps1 b/public/Set-DbaNetworkCertificate.ps1 index c84dfdcfa71..8a37c4764e0 100644 --- a/public/Set-DbaNetworkCertificate.ps1 +++ b/public/Set-DbaNetworkCertificate.ps1 @@ -316,7 +316,20 @@ function Set-DbaNetworkCertificate { if (-not $detailedCertTest.CertificateFound) { $failedChecks += "CertificateNotFound" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.KeyUsagesValid) { $failedChecks += "KeyUsagesInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.DnsNamesValid) { $failedChecks += "DnsNamesInvalid" } - if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PrivateKeyValid) { $failedChecks += "PrivateKeyInvalid" } + if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PrivateKeyValid) { + # SQL Server can only use a private key from a legacy Cryptographic Service Provider with KeySpec + # AT_KEYEXCHANGE; a Key Storage Provider (CNG) key - what New-SelfSignedCertificate creates by + # default - is refused by SQL Server itself. Say so, because "invalid" alone sends people looking + # at permissions. + $privateKeyDescription = "not accessible or a CNG (Key Storage Provider) key" + if ($detailedCertTest.PrivateKeyType) { + $privateKeyDescription = $detailedCertTest.PrivateKeyType + if ($null -ne $detailedCertTest.PrivateKeyNumber) { + $privateKeyDescription += " with KeyNumber $($detailedCertTest.PrivateKeyNumber)" + } + } + $failedChecks += "PrivateKeyInvalid (SQL Server needs a legacy CSP key with KeySpec AT_KEYEXCHANGE; this private key is $privateKeyDescription)" + } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.PublicKeyValid) { $failedChecks += "PublicKeyInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.SignatureAlgorithmValid) { $failedChecks += "SignatureAlgorithmInvalid" } if ($detailedCertTest.CertificateFound -and -not $detailedCertTest.EnhancedKeyUsageValid) { $failedChecks += "EnhancedKeyUsageInvalid" } diff --git a/public/Test-DbaNetworkCertificate.ps1 b/public/Test-DbaNetworkCertificate.ps1 index 91f0e51b1ad..d0bca4f465a 100644 --- a/public/Test-DbaNetworkCertificate.ps1 +++ b/public/Test-DbaNetworkCertificate.ps1 @@ -78,7 +78,7 @@ function Test-DbaNetworkCertificate { - CertificateFound: Boolean indicating if the certificate was found in LocalMachine\My - KeyUsagesValid: Boolean indicating if the certificate has the required key usages (DigitalSignature and KeyEncipherment) - DnsNamesValid: Boolean indicating if the certificate's DNS names include the server's network name - - PrivateKeyValid: Boolean indicating if the private key is RSACryptoServiceProvider with KeyNumber Exchange + - PrivateKeyValid: Boolean indicating if the private key is RSACryptoServiceProvider with KeyNumber Exchange (a legacy CSP key with KeySpec AT_KEYEXCHANGE, which SQL Server requires; CNG / Key Storage Provider keys are not supported by SQL Server) - PublicKeyValid: Boolean indicating if the public key is RSA with at least 2048 bits - SignatureAlgorithmValid: Boolean indicating if the signature algorithm is SHA-256, SHA-384, or SHA-512 - EnhancedKeyUsageValid: Boolean indicating if the certificate has the Server Authentication enhanced key usage diff --git a/tests/Set-DbaNetworkCertificate.Tests.ps1 b/tests/Set-DbaNetworkCertificate.Tests.ps1 index dadb32e2d32..58e937a2998 100644 --- a/tests/Set-DbaNetworkCertificate.Tests.ps1 +++ b/tests/Set-DbaNetworkCertificate.Tests.ps1 @@ -65,6 +65,34 @@ Describe $CommandName -Tag IntegrationTests { $WarnVar | Should -Match "No suitable certificate found" } + It "Says why a certificate with a CNG key is unsuitable" { + # New-SelfSignedCertificate creates a Key Storage Provider (CNG) key by default. SQL Server cannot use + # such a key, and the refusal has to name that reason instead of a bare PrivateKeyInvalid. + $newCngCertificate = { + param ($DnsName) + $splatCertificate = @{ + DnsName = $DnsName + CertStoreLocation = "Cert:\LocalMachine\My" + FriendlyName = "dbatoolsci_cng_key" + } + (New-SelfSignedCertificate @splatCertificate).Thumbprint + } + $splatCreateCng = @{ + ComputerName = $computerName + ScriptBlock = $newCngCertificate + ArgumentList = $computerName + # Raw, because Invoke-Command2 otherwise wraps the string in an object that only has a Length. + Raw = $true + } + $cngThumbprint = Invoke-Command2 @splatCreateCng + $script:createdNetworkCertificateThumbprints += $cngThumbprint + + $result = Set-DbaNetworkCertificate -SqlInstance $TestConfig.InstanceRestart -Thumbprint $cngThumbprint -WarningAction SilentlyContinue + $result | Should -BeNullOrEmpty + $WarnVar | Should -Match "PrivateKeyInvalid" + $WarnVar | Should -Match "legacy CSP key with KeySpec AT_KEYEXCHANGE" + } + It "applies an unsuitable certificate when Force is used" { $splatNewUnsuitableCertificate = @{ ComputerName = $computerName