Skip to content

Commit

Permalink
fix(pwsh): sort certification chain from leaf to root (#394)
Browse files Browse the repository at this point in the history
Issue: DGW-80
  • Loading branch information
CBenoit authored Mar 6, 2023
1 parent e3acafc commit f7ff93c
Show file tree
Hide file tree
Showing 16 changed files with 1,032 additions and 65 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -510,3 +510,25 @@ jobs:
run: |
Set-PSDebug -Trace 1
dotnet test utils/dotnet/GatewayUtils.sln
powershell-tests:
name: PowerShell module tests
runs-on: windows-2022
needs: preflight

steps:
- name: Checkout ${{ github.repository }}
uses: actions/checkout@v3
with:
ref: ${{ needs.preflight.outputs.ref }}

- name: Install Pester module
id: prepare
shell: pwsh
run: Install-Module Pester -Force

- name: Pester
shell: pwsh
run: |
Set-PSDebug -Trace 1
./powershell/run-tests.ps1
125 changes: 83 additions & 42 deletions powershell/DevolutionsGateway/Private/CertificateHelper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,18 @@ function ConvertTo-PemEncoding

$base64 = [Convert]::ToBase64String($RawData)

$offset = 0
$line_length = 64
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("-----BEGIN $label-----") | Out-Null
while ($offset -lt $base64.Length) {
$line_end = [Math]::Min($offset + $line_length, $base64.Length)
$sb.AppendLine($base64.Substring($offset, $line_end - $offset)) | Out-Null
$offset = $line_end
}
$sb.AppendLine("-----END $label-----") | Out-Null

return $sb.ToString().Trim()
$offset = 0
$line_length = 64
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("-----BEGIN $label-----") | Out-Null
while ($offset -lt $base64.Length) {
$line_end = [Math]::Min($offset + $line_length, $base64.Length)
$sb.AppendLine($base64.Substring($offset, $line_end - $offset)) | Out-Null
$offset = $line_end
}
$sb.AppendLine("-----END $label-----") | Out-Null

return $sb.ToString().Trim()
}

function ConvertFrom-PemEncoding
Expand Down Expand Up @@ -157,6 +157,34 @@ function Get-IsPemCertificateAuthority
return $false
}

function Get-CertificateIssuerName
{
[OutputType('string')]
param(
[Parameter(Mandatory=$true)]
[string] $PemData
)

$der = ConvertFrom-PemEncoding -Label 'CERTIFICATE' -PemData:$PemData
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]] $der)

return $cert.IssuerName.Name
}

function Get-CertificateSubjectName
{
[OutputType('string')]
param(
[Parameter(Mandatory=$true)]
[string] $PemData
)

$der = ConvertFrom-PemEncoding -Label 'CERTIFICATE' -PemData:$PemData
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]] $der)

return $cert.SubjectName.Name
}

function Get-PemCertificate
{
param(
Expand All @@ -165,55 +193,68 @@ function Get-PemCertificate
[string] $Password
)

[string[]] $PemChain = @()
$PrivateKey = $null

if (($CertificateFile -match ".pfx") -or ($CertificateFile -match ".p12")) {
$AsByteStream = if ($PSEdition -eq 'Core') { @{AsByteStream = $true} } else { @{'Encoding' = 'Byte'} }
$CertificateData = Get-Content -Path $CertificateFile -Raw @AsByteStream
$collection = [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]::new()
$collection.Import($CertificateData, $Password, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)

[string[]] $PemChain = @()
$PrivateKey = $null

foreach ($cert in $collection) {
if ($cert.HasPrivateKey) {
$PrivateKey = ConvertFrom-RsaPrivateKey -Rsa $cert.PrivateKey
}
$PemCert = ConvertTo-PemEncoding -Label 'CERTIFICATE' -RawData $cert.RawData
$PemChain += $PemCert
}

if ($PemChain.Count -eq 0) {
throw "Empty certificate chain!"
}
} else {
$PemData = Get-Content -Path $CertificateFile -Raw
$PemChain = Split-PemChain -Label 'CERTIFICATE' -PemData $PemData

if (Get-IsPemCertificateAuthority -PemData $PemChain[0]) {
[array]::Reverse($PemChain)
}

$Certificate = $PemChain -Join "`n"
$PrivateKey = Get-Content -Path $PrivateKeyFile -Raw
}

return [PSCustomObject]@{
Certificate = $Certificate
PrivateKey = $PrivateKey
}
if ($PemChain.Count -eq 0) {
throw "Empty certificate chain!"
}

$PemChain | ForEach-Object -Begin {
$Certs = @{}
} -Process {
$Key = Get-CertificateSubjectName -PemData $_
$Certs.Add($Key, $_)
} -End {
$Certs
}

$LeafCert = $PemChain | Where { -Not ( Get-IsPemCertificateAuthority -PemData $_ )}

[string[]] $SortedPemChain = @()

if ($LeafCert -Eq $null) {
# Do not apply any transformation to the provided chain if no leaf certificate is found
$SortedPemChain = $PemChain
} else {
$PemData = Get-Content -Path $CertificateFile -Raw
[string[]] $PemChain = Split-PemChain -Label 'CERTIFICATE' -PemData $PemData
# Otherwise, sort the chain: start by the leaf and then issued to issuer in order

if ($PemChain.Count -eq 0) {
throw "Empty certificate chain!"
}

if (Get-IsPemCertificateAuthority -PemData $PemChain[0]) {
[array]::Reverse($PemChain)
$SortedPemChain += $LeafCert
$IssuerName = Get-CertificateIssuerName -PemData $LeafCert
$SubjectName = Get-CertificateSubjectName -PemData $LeafCert

While ($Certs.ContainsKey($IssuerName) -And ($IssuerName -Ne $SubjectName)) {
$NextCert = $Certs[$IssuerName]
$SortedPemChain += $NextCert
$IssuerName = Get-CertificateIssuerName -PemData $NextCert
$SubjectName = Get-CertificateSubjectName -PemData $NextCert
}
}

$Certificate = $PemChain -Join "`n"
$PrivateKey = Get-Content -Path $PrivateKeyFile -Raw
$Certificate = $SortedPemChain -Join "`n"

return [PSCustomObject]@{
Certificate = $Certificate
PrivateKey = $PrivateKey
}
return [PSCustomObject]@{
Certificate = $Certificate
PrivateKey = $PrivateKey
}
}
28 changes: 8 additions & 20 deletions powershell/pester/Config.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@ Import-Module "$PSScriptRoot/../DevolutionsGateway"

Describe 'Devolutions Gateway config' {
InModuleScope DevolutionsGateway {
BeforeAll {
$ConfigPath = Join-Path $TestDrive 'Gateway'
}

Context 'Fresh environment' {
It 'Creates basic configuration' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
Set-DGatewayConfig -ConfigPath:$ConfigPath -Hostname 'gateway.local' -DockerRestartPolicy 'no'
$(Get-DGatewayConfig -ConfigPath:$ConfigPath).Hostname | Should -Be 'gateway.local'
}

It 'Sets gateway hostname' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
Set-DGatewayHostname -ConfigPath:$ConfigPath 'host1.gateway.local'
$(Get-DGatewayHostname -ConfigPath:$ConfigPath) | Should -Be 'host1.gateway.local'
}

It 'Sets gateway farm name' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
Set-DGatewayFarmName -ConfigPath:$ConfigPath 'farm.gateway.local'
$(Get-DGatewayFarmName -ConfigPath:$ConfigPath) | Should -Be 'farm.gateway.local'
}

It 'Sets gateway farm members' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
$FarmMembers = @('host1.gateway.local','host2.gateway.local','host3.gateway.local')
Set-DGatewayFarmMembers -ConfigPath:$ConfigPath $FarmMembers
$(Get-DGatewayFarmMembers -ConfigPath:$ConfigPath) | Should -Be $FarmMembers
}

It 'Sets gateway listeners' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
$HttpListener = New-DGatewayListener 'http://*:4040' 'http://*:4040'
$WsListener = New-DGatewayListener 'ws://*:4040' 'ws://*:4040'
$TcpListener = New-DGatewayListener 'tcp://*:4041' 'tcp://*:4041'
Expand All @@ -40,21 +43,6 @@ Describe 'Devolutions Gateway config' {
$ActualListeners = Get-DGatewayListeners -ConfigPath:$ConfigPath
$ExpectedListeners.Count | Should -Be $ActualListeners.Count
}
It 'Sets gateway application protocols' {
$ConfigPath = Join-Path $TestDrive 'Gateway'
Set-DGatewayApplicationProtocols -ConfigPath:$ConfigPath @('rdp')
$(Get-DGatewayApplicationProtocols -ConfigPath:$ConfigPath) | Should -Be @('rdp')
Set-DGatewayApplicationProtocols -ConfigPath:$ConfigPath @()
$(Get-DGatewayApplicationProtocols -ConfigPath:$ConfigPath) | Should -Be @()
}
It 'Starts Gateway' {
#$ConfigPath = Join-Path $TestDrive 'Gateway'
#Start-DGateway -ConfigPath:$ConfigPath -Verbose
}
It 'Stops Gateway' {
#$ConfigPath = Join-Path $TestDrive 'Gateway'
#Stop-DGateway -ConfigPath:$ConfigPath -Verbose
}
}
}
}
39 changes: 39 additions & 0 deletions powershell/pester/ImportCertificate.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Import-Module "$PSScriptRoot/../DevolutionsGateway"

Describe 'Devolutions Gateway certificate import' {
InModuleScope DevolutionsGateway {
BeforeAll {
$DummyPrivateKey = Join-Path $TestDrive 'dummy.key'
New-Item -Path $DummyPrivateKey -Value 'dummy'

$ConfigPath = Join-Path $TestDrive 'Gateway'
}

It 'Smoke' {
ForEach ($certFile in Get-ChildItem -Path ./ImportCertificate/WellOrdered) {
$expected = Get-Content -Path $certFile

Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $certFile -PrivateKeyFile $DummyPrivateKey

$resultingFile = Join-Path $TestDrive 'Gateway' 'server.crt'
$result = Get-Content -Path $resultingFile

$result | Should -Be $expected
}
}

It 'Sorting' {
ForEach ($unorderedCertFile in Get-ChildItem -Path ./ImportCertificate/Unordered) {
$wellOrderedCertFile = Join-Path './ImportCertificate/WellOrdered' $unorderedCertFile.Name
$expected = Get-Content -Path $wellOrderedCertFile

Import-DGatewayCertificate -ConfigPath:$ConfigPath -CertificateFile $unorderedCertFile -PrivateKeyFile $DummyPrivateKey

$resultingFile = Join-Path $TestDrive 'Gateway' 'server.crt'
$result = Get-Content -Path $ResultingFile

$result | Should -Be $expected
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
-----BEGIN CERTIFICATE-----
MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3
MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE
CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD
EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD
BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv
K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e
cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY
pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n
eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB
AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv
9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v
b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n
b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG
CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv
MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz
91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2
RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi
DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11
GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x
LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGjTCCBXWgAwIBAgIJAJIuXwD0Nx6OMA0GCSqGSIb3DQEBCwUAMIG0MQswCQYD
VQQGEwJVUzEQMA4GA1UECBMHQXJpem9uYTETMBEGA1UEBxMKU2NvdHRzZGFsZTEa
MBgGA1UEChMRR29EYWRkeS5jb20sIEluYy4xLTArBgNVBAsTJGh0dHA6Ly9jZXJ0
cy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5LzEzMDEGA1UEAxMqR28gRGFkZHkgU2Vj
dXJlIENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTIyMDcxOTAyNDYwNVoX
DTIzMDgyMDAyNDYwNVowGDEWMBQGA1UEAwwNKi5hdXRvLWl0LmNvbTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOwZDHCg+wJM4spwxsp01hyQB4V3xpQe
ZHTvYv27ndE3dfScSLBpcDamBotpMjR8v77NmS9H3BY2sogmixLB3cECBp/dhWVL
KxbpWY00EcMXgNfYiRzlynShXubGj7ok1wt/6IOsPKwjRvVXcssuPsDtOziUuI7G
GFuvXgu+JsN+4Cu0z1mNo2v2Oi2tisKmGIdtJVoQf8cAs4Ov0gwH4VLVSAdvvHmu
PvveqMhvlAIaBO4XixUSLLIJvGpsz7huWjidmcqus0Vu9qA+DUNudJ19vlJfE49L
FjQd0FWa6jfSHihFQvvDvzATXCFbUdn82ur6Ci2JZFhPyCj5r2p2YHUCAwEAAaOC
AzswggM3MAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMA4GA1UdDwEB/wQEAwIFoDA4BgNVHR8EMTAvMC2gK6AphidodHRwOi8vY3Js
LmdvZGFkZHkuY29tL2dkaWcyczEtNDI5NS5jcmwwXQYDVR0gBFYwVDBIBgtghkgB
hv1tAQcXATA5MDcGCCsGAQUFBwIBFitodHRwOi8vY2VydGlmaWNhdGVzLmdvZGFk
ZHkuY29tL3JlcG9zaXRvcnkvMAgGBmeBDAECATB2BggrBgEFBQcBAQRqMGgwJAYI
KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmdvZGFkZHkuY29tLzBABggrBgEFBQcwAoY0
aHR0cDovL2NlcnRpZmljYXRlcy5nb2RhZGR5LmNvbS9yZXBvc2l0b3J5L2dkaWcy
LmNydDAfBgNVHSMEGDAWgBRAwr0njsw0gzCiM9f7bLPwtCyAzjAlBgNVHREEHjAc
gg0qLmF1dG8taXQuY29tggthdXRvLWl0LmNvbTAdBgNVHQ4EFgQUcXE3BKYV+XUU
9n83E29BZ64b30IwggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB3AOg+0No+9QY1
MudXKLyJa8kD08vREWvs62nhd31tBr1uAAABghRZRPcAAAQDAEgwRgIhAMEoD0PB
zhkAlpXxjqf20u7noeAXbrY1Dx0wYXvLGorxAiEAr7No8wiAKiqM98RDRatYIbjh
w19GM57H0oTTBQJCtTUAdQA1zxkbv7FsV78PrUxtQsu7ticgJlHqP+Eq76gDwzvW
TAAAAYIUWUaWAAAEAwBGMEQCIBI/77mZInm0t174owl2uEjP0TOtOoUW8u1vtZ+j
eqMoAiA7++SIkU4A9LyhOqKskvTwAXuuUazErUQZd6P435llowB2AHoyjFTYty22
IOo44FIe6YQWcDIThU070ivBOlejUutSAAABghRZR2wAAAQDAEcwRQIhAMS3emzQ
uxxIgX2RMr6wMlrGNIwAQ5Ti0XHe7Yu+jbIGAiAbqBLuypuyOk/FmZ8VHxr4/p0h
RHoGQKakkQhmV5TFlzANBgkqhkiG9w0BAQsFAAOCAQEAbmJpfVS6urL+Qia8hCzj
OpMPeyp6lWG7Nj+JfDWPGdisNyWG65PGqPOA+WfPy8LoHXJvvYfzWRIAbGv5KNlS
Q2dWkhwW4/DaBq1loyi0tFHZOqskxbeSP2N3TNATOyMAIPCHgS+x3Gg3Ueh10dg5
e2yf3vc29oXWg2Rdn1tGCsefQG7TtbvkoGp1/MGiQUDjYfbODYOT1atC3HtFJ8/R
Np0luPgqrYr6UZJPOdFoNgupae9jlAfNeU7Vyqt6cwCHSwnXEg4gIjbCIpjNYoC6
zntjZRm1xldxxqZuJaE1jANIM4tI7oT4fhKhVv6twp3PYRHIPGTB4Kchzd7eLiit
iA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
4uJEvlz36hz1
-----END CERTIFICATE-----
Loading

0 comments on commit f7ff93c

Please sign in to comment.