Skip to content

Commit a45a617

Browse files
New-DbaComputerCertificate, New-DbaComputerCertificateSigningRequest - Add -Provider to create the private key in a Key Storage Provider (#10722)
1 parent 2b67215 commit a45a617

4 files changed

Lines changed: 154 additions & 11 deletions

‎public/New-DbaComputerCertificate.ps1‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ function New-DbaComputerCertificate {
66
.DESCRIPTION
77
Creates a new computer certificate - self-signed or signed by an Active Directory CA, using the Web Server certificate.
88
9-
By default, a key with a length of 2048 bits and a friendly name of "SQL Server" is generated.
9+
By default, a key with a length of 2048 bits and a friendly name of "SQL Server" is generated. The private key
10+
is created by the legacy Microsoft RSA SChannel Cryptographic Provider unless -Provider asks for a Key Storage Provider.
1011
1112
This command was originally intended to help automate the process so that SSL certificates can be available for enforcing encryption on connections.
1213
@@ -62,6 +63,12 @@ function New-DbaComputerCertificate {
6263
Defaults to 2048 bits which meets current industry security standards for production environments.
6364
4096 bits can be used for high-security environments, though it may slightly impact performance during SSL handshakes.
6465
66+
.PARAMETER Provider
67+
Specifies the cryptographic provider that generates and holds the private key.
68+
Defaults to "Microsoft RSA SChannel Cryptographic Provider", a legacy Cryptographic Service Provider (CSP) that creates the key with KeySpec AT_KEYEXCHANGE, which is what the Microsoft certificate requirements for SQL Server name.
69+
Use "Microsoft Software Key Storage Provider" for a Cryptography Next Generation (CNG) key. SQL Server 2019 and later load such a key as well, and Set-DbaNetworkCertificate and Test-DbaNetworkCertificate handle both key types.
70+
The key is generated on the machine that runs the command and travels with the PFX to a remote computer, so the provider is the same on the target.
71+
6572
.PARAMETER Store
6673
Specifies the certificate store location where the certificate will be installed.
6774
Defaults to "LocalMachine" which makes certificates available to services like SQL Server.
@@ -213,6 +220,8 @@ function New-DbaComputerCertificate {
213220
[string]$FriendlyName = "SQL Server",
214221
[string]$CertificateTemplate = "WebServer",
215222
[int]$KeyLength = 2048,
223+
[ValidateSet("Microsoft RSA SChannel Cryptographic Provider", "Microsoft Software Key Storage Provider")]
224+
[string]$Provider = "Microsoft RSA SChannel Cryptographic Provider",
216225
[string]$Store = "LocalMachine",
217226
[string]$Folder = "My",
218227
[ValidateSet("EphemeralKeySet", "Exportable", "PersistKeySet", "UserProtected", "NonExportable")]
@@ -400,7 +409,10 @@ function New-DbaComputerCertificate {
400409
Add-Content $certCfg 'Signature="$Windows NT$"'
401410
Add-Content $certCfg "[NewRequest]"
402411
Add-Content $certCfg "Subject = ""CN=$fqdn"""
403-
Add-Content $certCfg "KeySpec = 1"
412+
if ($Provider -eq "Microsoft RSA SChannel Cryptographic Provider") {
413+
# A legacy CSP key. KeySpec 1 is AT_KEYEXCHANGE, the KeySpec the Microsoft certificate requirements for SQL Server name.
414+
Add-Content $certCfg "KeySpec = 1"
415+
}
404416
Add-Content $certCfg "KeyLength = $KeyLength"
405417
# Keep the source cert exportable whenever it must be copied to another host.
406418
if ("NonExportable" -in $Flag -and -not $ClusterInstanceName -and $computer.IsLocalHost) {
@@ -414,8 +426,14 @@ function New-DbaComputerCertificate {
414426
Add-Content $certCfg "PrivateKeyArchive = FALSE"
415427
Add-Content $certCfg "UserProtected = FALSE"
416428
Add-Content $certCfg "UseExistingKeySet = FALSE"
417-
Add-Content $certCfg "ProviderName = ""Microsoft RSA SChannel Cryptographic Provider"""
418-
Add-Content $certCfg "ProviderType = 12"
429+
Add-Content $certCfg "ProviderName = ""$Provider"""
430+
if ($Provider -eq "Microsoft RSA SChannel Cryptographic Provider") {
431+
# ProviderType 12 is PROV_RSA_SCHANNEL.
432+
Add-Content $certCfg "ProviderType = 12"
433+
} else {
434+
# A Key Storage Provider has neither a provider type nor a KeySpec, it takes the key algorithm instead.
435+
Add-Content $certCfg "KeyAlgorithm = RSA"
436+
}
419437
if ($SelfSigned) {
420438
Add-Content $certCfg "RequestType = Cert"
421439
Add-Content $certCfg "NotBefore = $((Get-Date).ToShortDateString())"

‎public/New-DbaComputerCertificateSigningRequest.ps1‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function New-DbaComputerCertificateSigningRequest {
1010
1111
Supports both standalone SQL Server instances and cluster configurations, automatically resolving FQDNs and configuring appropriate DNS entries. The generated certificates work with SQL Server's encryption features including encrypted client connections, mirroring, and backup encryption scenarios.
1212
13-
By default, creates RSA certificates with 2048-bit keys, the minimum the dbatools network certificate commands accept as suitable for SQL Server, though this can be raised for stronger encryption requirements. All certificates are configured as machine certificates with the Microsoft RSA SChannel Cryptographic Provider for compatibility with SQL Server's encryption stack.
13+
By default, creates RSA certificates with 2048-bit keys, the minimum the dbatools network certificate commands accept as suitable for SQL Server, though this can be raised for stronger encryption requirements. All certificates are configured as machine certificates, by default with the legacy Microsoft RSA SChannel Cryptographic Provider; -Provider switches the private key to the Microsoft Software Key Storage Provider.
1414
1515
.PARAMETER ComputerName
1616
The target computer name hosting the SQL Server instance where the certificate will be installed. Accepts multiple computer names for batch processing.
@@ -35,6 +35,11 @@ function New-DbaComputerCertificateSigningRequest {
3535
Specifies the RSA key length in bits for the certificate. Defaults to 2048, the minimum key length Test-DbaNetworkCertificate accepts as suitable for SQL Server; 4096 is possible for stronger requirements.
3636
Higher key lengths provide stronger encryption but may impact SQL Server connection performance on older hardware.
3737
38+
.PARAMETER Provider
39+
Specifies the cryptographic provider that generates and holds the private key of the request.
40+
Defaults to "Microsoft RSA SChannel Cryptographic Provider", a legacy Cryptographic Service Provider (CSP) that creates the key with KeySpec AT_KEYEXCHANGE, which is what the Microsoft certificate requirements for SQL Server name.
41+
Use "Microsoft Software Key Storage Provider" for a Cryptography Next Generation (CNG) key. SQL Server 2019 and later load such a key as well, and Set-DbaNetworkCertificate and Test-DbaNetworkCertificate handle both key types.
42+
3843
.PARAMETER Dns
3944
Additional DNS names to include in the certificate's Subject Alternative Name (SAN) field. By default includes both short and FQDN names.
4045
Add extra DNS entries here if clients connect using aliases, load balancer names, or other DNS records that point to your SQL Server instance.
@@ -109,6 +114,8 @@ function New-DbaComputerCertificateSigningRequest {
109114
[string]$Path = (Get-DbatoolsConfigValue -FullName 'Path.DbatoolsExport'),
110115
[string]$FriendlyName = "SQL Server",
111116
[int]$KeyLength = 2048,
117+
[ValidateSet("Microsoft RSA SChannel Cryptographic Provider", "Microsoft Software Key Storage Provider")]
118+
[string]$Provider = "Microsoft RSA SChannel Cryptographic Provider",
112119
[string[]]$Dns,
113120
[switch]$EnableException
114121
)
@@ -228,7 +235,10 @@ function New-DbaComputerCertificateSigningRequest {
228235
Add-Content $certCfg 'Signature="$Windows NT$"'
229236
Add-Content $certCfg "[NewRequest]"
230237
Add-Content $certCfg "Subject = ""CN=$fqdn"""
231-
Add-Content $certCfg "KeySpec = 1"
238+
if ($Provider -eq "Microsoft RSA SChannel Cryptographic Provider") {
239+
# A legacy CSP key. KeySpec 1 is AT_KEYEXCHANGE, the KeySpec the Microsoft certificate requirements for SQL Server name.
240+
Add-Content $certCfg "KeySpec = 1"
241+
}
232242
Add-Content $certCfg "KeyLength = $KeyLength"
233243
Add-Content $certCfg "Exportable = TRUE"
234244
Add-Content $certCfg "MachineKeySet = TRUE"
@@ -237,8 +247,14 @@ function New-DbaComputerCertificateSigningRequest {
237247
Add-Content $certCfg "PrivateKeyArchive = FALSE"
238248
Add-Content $certCfg "UserProtected = FALSE"
239249
Add-Content $certCfg "UseExistingKeySet = FALSE"
240-
Add-Content $certCfg "ProviderName = ""Microsoft RSA SChannel Cryptographic Provider"""
241-
Add-Content $certCfg "ProviderType = 12"
250+
Add-Content $certCfg "ProviderName = ""$Provider"""
251+
if ($Provider -eq "Microsoft RSA SChannel Cryptographic Provider") {
252+
# ProviderType 12 is PROV_RSA_SCHANNEL.
253+
Add-Content $certCfg "ProviderType = 12"
254+
} else {
255+
# A Key Storage Provider has neither a provider type nor a KeySpec, it takes the key algorithm instead.
256+
Add-Content $certCfg "KeyAlgorithm = RSA"
257+
}
242258
if ($SelfSigned) {
243259
Add-Content $certCfg "RequestType = Cert"
244260
} else {

‎tests/New-DbaComputerCertificate.Tests.ps1‎

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Describe $CommandName -Tag UnitTests {
2020
"FriendlyName",
2121
"CertificateTemplate",
2222
"KeyLength",
23+
"Provider",
2324
"Store",
2425
"Folder",
2526
"Flag",
@@ -87,11 +88,43 @@ Describe $CommandName -Tag UnitTests {
8788
$script:requestConfig | Should -Contain "Exportable = TRUE"
8889
$script:requestConfig | Should -Not -Contain "Exportable = FALSE"
8990
}
91+
92+
It "Writes a Key Storage Provider request when Provider asks for one" {
93+
$splatKspCertificate = @{
94+
ComputerName = "dbatools-review-remote"
95+
CaServer = "dbatools-ca"
96+
CaName = "dbatools-ca"
97+
Provider = "Microsoft Software Key Storage Provider"
98+
WhatIf = $true
99+
}
100+
$null = New-DbaComputerCertificate @splatKspCertificate
101+
102+
$script:requestConfig | Should -Contain "ProviderName = ""Microsoft Software Key Storage Provider"""
103+
$script:requestConfig | Should -Contain "KeyAlgorithm = RSA"
104+
$script:requestConfig | Should -Not -Contain "KeySpec = 1"
105+
$script:requestConfig | Should -Not -Contain "ProviderType = 12"
106+
}
107+
108+
It "Writes a legacy CSP request with KeySpec AT_KEYEXCHANGE by default" {
109+
$splatDefaultCertificate = @{
110+
ComputerName = "dbatools-review-remote"
111+
CaServer = "dbatools-ca"
112+
CaName = "dbatools-ca"
113+
WhatIf = $true
114+
}
115+
$null = New-DbaComputerCertificate @splatDefaultCertificate
116+
117+
$script:requestConfig | Should -Contain "ProviderName = ""Microsoft RSA SChannel Cryptographic Provider"""
118+
$script:requestConfig | Should -Contain "ProviderType = 12"
119+
$script:requestConfig | Should -Contain "KeySpec = 1"
120+
$script:requestConfig | Should -Not -Contain "KeyAlgorithm = RSA"
121+
}
90122
}
91123
}
92124

93-
#Tests do not run in appveyor
94-
Describe $CommandName -Tag IntegrationTests -Skip:([bool]$env:appveyor) {
125+
# These tests used to be skipped when APPVEYOR is set, which the Azure lanes do as well. Creating a self-signed
126+
# certificate in LocalMachine\My works on the runners, so they run there now.
127+
Describe $CommandName -Tag IntegrationTests {
95128
Context "Can generate a new certificate with default settings" {
96129
BeforeAll {
97130
$defaultCert = New-DbaComputerCertificate -SelfSigned -EnableException
@@ -157,4 +190,62 @@ Describe $CommandName -Tag IntegrationTests -Skip:([bool]$env:appveyor) {
157190
"$($documentCert.EnhancedKeyUsageList)" -match "1\.3\.6\.1\.5\.5\.7\.3\.1" | Should -BeFalse
158191
}
159192
}
193+
194+
Context "Can generate a certificate with a Key Storage Provider key" {
195+
BeforeAll {
196+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
197+
198+
# The command returns a copy of the certificate object without the key, so the private key is read from the store entry.
199+
$kspCert = New-DbaComputerCertificate -SelfSigned -Provider "Microsoft Software Key Storage Provider"
200+
$kspStoreCert = Get-ChildItem -Path "Cert:\LocalMachine\My\$($kspCert.Thumbprint)"
201+
$kspKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($kspStoreCert)
202+
203+
$cspCert = New-DbaComputerCertificate -SelfSigned
204+
$cspStoreCert = Get-ChildItem -Path "Cert:\LocalMachine\My\$($cspCert.Thumbprint)"
205+
$cspKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cspStoreCert)
206+
207+
# For a remote computer the command exports a PFX and imports it on the target. The provider has to survive that transfer.
208+
# On CI the instance is local, so the transfer only happens against a lab whose instance runs on another computer.
209+
$computerName = ([DbaInstanceParameter]$TestConfig.InstanceSingle).ComputerName
210+
$remoteKspCert = New-DbaComputerCertificate -ComputerName $computerName -SelfSigned -Provider "Microsoft Software Key Storage Provider"
211+
$readProvider = {
212+
param ($Thumbprint)
213+
$cert = Get-ChildItem -Path "Cert:\LocalMachine\My\$Thumbprint"
214+
([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)).Key.Provider.Provider
215+
}
216+
$splatReadProvider = @{
217+
ComputerName = $computerName
218+
ScriptBlock = $readProvider
219+
ArgumentList = $remoteKspCert.Thumbprint
220+
# Raw, because Invoke-Command2 otherwise wraps the string in an object that only has a Length.
221+
Raw = $true
222+
}
223+
$remoteProvider = Invoke-Command2 @splatReadProvider
224+
225+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
226+
}
227+
228+
AfterAll {
229+
$PSDefaultParameterValues["*-Dba*:EnableException"] = $true
230+
Remove-DbaComputerCertificate -Thumbprint $kspCert.Thumbprint, $cspCert.Thumbprint
231+
Remove-DbaComputerCertificate -ComputerName $computerName -Thumbprint $remoteKspCert.Thumbprint
232+
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
233+
}
234+
235+
It "Holds the private key in the Key Storage Provider" {
236+
$kspKey.Key.Provider.Provider | Should -Be "Microsoft Software Key Storage Provider"
237+
Test-Path -Path "$env:ProgramData\Microsoft\Crypto\Keys\$($kspKey.Key.UniqueName)" -PathType Leaf | Should -BeTrue
238+
}
239+
240+
It "Holds the private key in the legacy CSP with KeySpec AT_KEYEXCHANGE by default" {
241+
$cspKey.Key.Provider.Provider | Should -Be "Microsoft RSA SChannel Cryptographic Provider"
242+
# A legacy CSP key opened through CNG reports an AT_KEYEXCHANGE KeySpec as AllUsages, an AT_SIGNATURE one as Signing.
243+
$cspKey.Key.KeyUsage | Should -Be "AllUsages"
244+
Test-Path -Path "$env:ProgramData\Microsoft\Crypto\RSA\MachineKeys\$($cspKey.Key.UniqueName)" -PathType Leaf | Should -BeTrue
245+
}
246+
247+
It "Keeps the Key Storage Provider key when the certificate is imported on a remote computer" {
248+
$remoteProvider | Should -Be "Microsoft Software Key Storage Provider"
249+
}
250+
}
160251
}

‎tests/New-DbaComputerCertificateSigningRequest.Tests.ps1‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Describe $CommandName -Tag UnitTests {
1717
"Path",
1818
"FriendlyName",
1919
"KeyLength",
20+
"Provider",
2021
"Dns",
2122
"EnableException"
2223
)
@@ -33,6 +34,7 @@ Describe $CommandName -Tag IntegrationTests {
3334
$requestPath = "$($TestConfig.Temp)\$CommandName-$(Get-Random)"
3435
$null = New-Item -Path $requestPath -ItemType Directory
3536
$requestFriendlyName = "dbatoolsci_csr_$(Get-Random)"
37+
$kspRequestFriendlyName = "dbatoolsci_csr_ksp_$(Get-Random)"
3638

3739
# We want to run all commands outside of the BeforeAll block without EnableException to be able to test for specific warnings.
3840
$PSDefaultParameterValues.Remove("*-Dba*:EnableException")
@@ -45,7 +47,7 @@ Describe $CommandName -Tag IntegrationTests {
4547
# certreq -new leaves the pending request with its private key in the REQUEST store of the local machine.
4648
$requestStore = New-Object System.Security.Cryptography.X509Certificates.X509Store("REQUEST", "LocalMachine")
4749
$requestStore.Open("ReadWrite")
48-
foreach ($pendingRequest in ($requestStore.Certificates | Where-Object FriendlyName -eq $requestFriendlyName)) {
50+
foreach ($pendingRequest in ($requestStore.Certificates | Where-Object FriendlyName -in $requestFriendlyName, $kspRequestFriendlyName)) {
4951
$requestStore.Remove($pendingRequest)
5052
}
5153
$requestStore.Close()
@@ -66,4 +68,20 @@ Describe $CommandName -Tag IntegrationTests {
6668
(certutil -dump $signingRequest.FullName) -join " " | Should -Match "Public Key Length: 2048 bits"
6769
$WarnVar | Should -BeNullOrEmpty
6870
}
71+
72+
It "Generates the key in the Key Storage Provider when Provider asks for it" {
73+
# certreq keeps the key of a pending request with the request in LocalMachine\REQUEST, which is where the provider shows.
74+
$splatKspRequest = @{
75+
Path = $requestPath
76+
FriendlyName = $kspRequestFriendlyName
77+
Provider = "Microsoft Software Key Storage Provider"
78+
}
79+
$files = New-DbaComputerCertificateSigningRequest @splatKspRequest
80+
$files.Count | Should -Be 2
81+
$pendingRequest = Get-ChildItem -Path Cert:\LocalMachine\REQUEST | Where-Object FriendlyName -eq $kspRequestFriendlyName
82+
$pendingRequest | Should -Not -BeNullOrEmpty
83+
$privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($pendingRequest)
84+
$privateKey.Key.Provider.Provider | Should -Be "Microsoft Software Key Storage Provider"
85+
$WarnVar | Should -BeNullOrEmpty
86+
}
6987
}

0 commit comments

Comments
 (0)