Validation/Test-ReadinessChecker.Tests.ps1
param( [switch]$TestMFA = $false, [switch]$keepLogs ) $global:KeepLogs = $KeepLogs $ErrorActionPreference = 'Stop' $here = Split-Path -Parent $MyInvocation.MyCommand.Path Import-Module .\PesterHelper.psm1 -Force -DisableNameChecking Write-Verbose "Import Data" $testData = Import-LocalizedData -BaseDirectory $here -FileName PesterHelperData.psd1 $GLOBAL:AllCertificates = $testData.AllCertificates $Global:AzsReadinessLogFile = Join-Path -Path $ENV:TEMP -ChildPath 'AzsReadinessChecker.log' $GLOBAL:filesForCleanUp = @() Install-Module AzureRM.KeyVault -Force #Connect to Azure Connect-Azure #Get JSON $keyvaultName = $testdata.pesterdata.ptkkvName $jsonPath = Save-File -VaultName $keyvaultName -secretName $testdata.DeploymentData -outputPath $ENV:Temp\DeploymentData.json $GLOBAL:filesForCleanUp += $jsonPath $CustomAzureEnvironmentUri = Get-PlainTextSecret -VaultName $keyvaultName -secretName $testData.CustomAzureEnvironment $deploymentDataJSON = Get-Content -Path $jsonPath | ConvertFrom-Json $regionName = $deploymentDataJSON.DeploymentData.RegionName $externalFQDN = $deploymentDataJSON.DeploymentData.ExternalDomainFQDN Describe -Name "Certificate Validation" { BeforeAll{ #Download certificates neccessary Connect-Azure $AllCertificateNames = $testdata.AllCertificates.Keys Set-SecurityProtocol -securityProtocol $targetSecurityProtocol $certificateSecrets = Get-AzureKeyVaultSecret -VaultName $keyvaultName | Where-Object Name -in $AllCertificateNames Set-SecurityProtocol -securityProtocol $restoreSecProtocol $i = 1 $pwd = New-RandomPassword foreach ($secret in $certificateSecrets) { [int]$percentageComplete = $i / $certificateSecrets.count * 100 Write-Progress -Activity "Downloading Certificates" -Status "$percentageComplete% Complete:" -PercentComplete $percentageComplete -CurrentOperation $secret.Name $destFile = "$env:TEMP\$($secret.Name).PFX" $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName $secret.Name -outputPath $destFile -asSecret:$true $AllCertificates[$secret.name].pfxPath = $pfx.pfxPath $AllCertificates[$secret.name].pfxPassword = $pfx.pfxPassword $i++ $GLOBAL:filesForCleanUp += $pfx.pfxPath } Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force -DisableNameChecking # Get Deployment Certificate data if (Get-ChildItem c:\Windows\System32\CertPKICmdlet.dll | Where-Object {$_.versioninfo.ProductVersion -ge '10.0.17134.1'}){ $GoodPfxEncryptionPath = "$env:TEMP\pfxEncryptionPass.pfx" $BadPfxEncryptionPath = "$env:TEMP\pfxEncryptionFail.pfx" $tempCert = Import-PfxCertificate -Exportable -Password $AllCertificates['MissingDNSName'].pfxPassword -CertStoreLocation 'Cert:\LocalMachine\My' -FilePath $AllCertificates['MissingDNSName'].pfxPath Export-PfxCertificate -NoProperties -Force -CryptoAlgorithmOption TripleDES_SHA1 -ChainOption BuildChain -Password $AllCertificates['MissingDNSName'].pfxPassword -Cert $tempCert -FilePath $GoodPfxEncryptionPath Export-PfxCertificate -NoProperties -Force -CryptoAlgorithmOption AES256_SHA256 -ChainOption BuildChain -Password $AllCertificates['MissingDNSName'].pfxPassword -Cert $tempCert -FilePath $BadPfxEncryptionPath $GLOBAL:filesForCleanUp += $GoodPfxEncryptionPath } else { Write-Verbose -verbose "Skipping encryption test" } } Context -Name "Key Usage" { It "Bad Key Usage Certificates Should Fail" { $certConfig = @{ DNSName = $AllCertificates['BadUsage'].ExpectedPrefix IncludeTests = 'Key Usage' ExcludeTests = 'CNG Key' } Test-KeyUsage -cert (Get-x509 -certinfo $AllCertificates['BadUsage']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Bad Key Usage Certificates Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['BadUsage'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Key Usage' } Test-KeyUsage -cert (Get-x509 -certinfo $AllCertificates['BadUsage']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } It "Bad Key Usage declare should fail with message" { $keyUsage = 'CRL Sign' $certConfig = @{ DNSName = $AllCertificates['BadSignature'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key' KeyUsage = $keyUsage } $result = Test-KeyUsage -cert (Get-x509 -certinfo $AllCertificates['BadSignature']) -certConfig $certConfig $result | Select-Object -ExpandProperty Result | should be "Fail" $failureDetails = $result | Select-Object -ExpandProperty FailureDetail $failureDetails -match $keyUsage | should be $true } } Context -Name "Signature" { It "Bad Signature Certificates Should Fail" { $certConfig = @{ DNSName = $AllCertificates['BadSignature'].ExpectedPrefix IncludeTests = 'Signature Algorithm' ExcludeTests = 'CNG Key' } Test-SignatureAlgorithm -x509 (Get-x509 -certInfo $AllCertificates['BadSignature']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Bad Signature Certificates Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['BadSignature'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Signature Algorithm' } Test-SignatureAlgorithm -x509 (Get-x509 -certInfo $AllCertificates['BadSignature']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } It "Bad Signature declared Should fail with message" { $certConfig = @{ DNSName = $AllCertificates['BadSignature'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key' HashAlgorithm = 'SHA1RSA','SHA256RSA' } $result = Test-SignatureAlgorithm -x509 (Get-x509 -certInfo $AllCertificates['BadSignature']) -certConfig $certConfig $result | Select-Object -ExpandProperty Result | should be "Fail" $result | Select-Object -ExpandProperty FailureDetail | should BeLike "*Please avoid using SHA1RSA,SHA256RSA signature algorithm(s)*" } } Context -Name "Private Key" { It "Bad Private Key Certificates Should Fail" { $certConfig = @{ DNSName = $AllCertificates['BadPrivateKey'].ExpectedPrefix IncludeTests = 'Private Key' ExcludeTests = 'CNG Key' } Test-PrivateKey -x509 (Get-x509 -certInfo $AllCertificates['BadPrivateKey']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Bad Private Key Certificates Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['BadPrivateKey'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Private Key' } Test-PrivateKey -x509 (Get-x509 -certInfo $AllCertificates['BadPrivateKey']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } } Context -Name "Chain" { It "Incomplete Certificate Chain Should Fail" { $certConfig = @{ DNSName = $AllCertificates['IncompleteChain'].ExpectedPrefix IncludeTests = 'Cert Chain' ExcludeTests = 'CNG Key' } Test-CertificateChain -pfxdata (Get-PFXHash -certInfo $AllCertificates['IncompleteChain']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Incomplete Certificate Chain Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['IncompleteChain'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Cert Chain' } Test-CertificateChain -pfxdata (Get-PFXHash -certInfo $AllCertificates['IncompleteChain']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } It "Wrong Certificate Chain Order Should Fail" { $certConfig = @{ DNSName = $AllCertificates['BadChainOrder'].ExpectedPrefix IncludeTests = 'Chain Order' ExcludeTests = 'CNG Key' } Test-CertificateChainOrder -pfxdata (Get-PFXHash -certInfo $AllCertificates['BadChainOrder']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Wrong Certificate Chain Order Should SkippedByCOnfig" { $certConfig = @{ DNSName = $AllCertificates['BadChainOrder'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Chain Order' } Test-CertificateChainOrder -pfxdata (Get-PFXHash -certInfo $AllCertificates['BadChainOrder']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } It "Untrusted Chain Should Fail" { $certConfig = @{ DNSName = $AllCertificates['UntrustedChain'].ExpectedPrefix IncludeTests = 'Trusted Chain' ExcludeTests = 'CNG Key' } Test-TrustedChain -cert (Get-x509 -certInfo $AllCertificates['UntrustedChain']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Untrusted Chain Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['UntrustedChain'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Trusted Chain' } Test-TrustedChain -cert (Get-x509 -certInfo $AllCertificates['UntrustedChain']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } } Context -Name "Other Certificates" { It "Other certificates present Should Fail" { $certConfig = @{ DNSName = $AllCertificates['OtherCertificates'].ExpectedPrefix IncludeTests = 'Other Certificates' ExcludeTests = 'CNG Key' } Test-OtherCertificates -pfxdata (Get-PFXHash -certInfo $AllCertificates['OtherCertificates']) -certConfig $certConfig -expectedFQDN $AllCertificates['othercertificates'].ExpectedFQDN | Select-Object -ExpandProperty Result | should be "WARNING" } It "Other certificates should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['OtherCertificates'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Other Certificates' } Test-OtherCertificates -pfxdata (Get-PFXHash -certInfo $AllCertificates['OtherCertificates']) -certConfig $certConfig -expectedFQDN $AllCertificates['othercertificates'].ExpectedFQDN | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } } Context -Name "KeySize" { It "Key Size less than 2048 Should Fail" { $certConfig = @{ DNSName = $AllCertificates['BadKeySize'].ExpectedPrefix IncludeTests = 'Key Length' ExcludeTests = 'CNG Key' } Test-KeySize -cert (Get-x509 -certInfo $AllCertificates['BadKeySize']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Key Size Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['BadKeySize'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Key Length' } Test-KeySize -cert (Get-x509 -certInfo $AllCertificates['BadKeySize']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } It "Key Size declared key size Should Fail with message" { $keylengthLimit = 4096 $certConfig = @{ DNSName = $AllCertificates['BadKeySize'].ExpectedPrefix IncludeTests = 'Key Length' ExcludeTests = 'CNG Key' KeyLength = $keylengthLimit } $result = Test-KeySize -cert (Get-x509 -certInfo $AllCertificates['BadKeySize']) -certConfig $certConfig $result | Select-Object -ExpandProperty Result | should be "Fail" $result | Select-Object -ExpandProperty FailureDetail | should BeLike "*Key size should be $keylengthLimit or higher*" } } Context -Name "DNSName" { It "Missing DNS Name Should Fail" { $certConfig = @{ DNSName = $AllCertificates['MissingDNSName'].ExpectedPrefix IncludeTests = 'DNS Names' ExcludeTests = 'CNG Key' } Test-DNSNames -cert (Get-x509 -certInfo $AllCertificates['MissingDNSName']) -certConfig $certConfig -ExpectedDomainFQDN $AllCertificates['MissingDNSName'].ExpectedFQDN | Select-Object -ExpandProperty Result | should be "Fail" } It "Missing DNS Name Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['ExpiredCert'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','DNS Names' } Test-DNSNames -cert (Get-x509 -certInfo $AllCertificates['MissingDNSName']) -certConfig $certConfig -ExpectedDomainFQDN $AllCertificates['MissingDNSName'].ExpectedFQDN | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } } Context -Name "Expiry" { It "Expired Certificate should fail" { $certConfig = @{ DNSName = $AllCertificates['ExpiredCert'].ExpectedPrefix IncludeTests = 'Expiry Date' ExcludeTests = 'CNG Key' } Test-CertificateExpiry -cert (Get-x509 -certInfo $AllCertificates['ExpiredCert']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "Fail" } It "Expired Certificate Should SkipByConfig" { $certConfig = @{ DNSName = $AllCertificates['ExpiredCert'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'CNG Key','Expiry Date' } Test-CertificateExpiry -cert (Get-x509 -certInfo $AllCertificates['ExpiredCert']) -certConfig $certConfig | Select-Object -ExpandProperty Result | should be "SkippedByConfig" } } Context -Name "CNG" { It "PaaS Certificates should fail CNG Keys" { $certConfig = @{ DNSName = $AllCertificates['CngKey'].ExpectedPrefix IncludeTests = 'All' ExcludeTests = 'foo' } $cngResult = Test-PrivateKey -cert (Get-x509 -certinfo $AllCertificates['CngKey']) -IsPaaS $cngResult | Select-Object -ExpandProperty Result | should be "Fail" $cngResult | Select-Object -ExpandProperty FailureDetail | should match 'CNG Certificate detected, support for this certificate type may not currently be available' } } if ($GoodPfxEncryptionPath -and $badPfxEncryptionPath){ Context -Name "PFXEncryption"{ It "PFX Encryption Should fail"{ Test-PfxEncryption -pfxfile $BadPfxEncryptionPath -pfxPassword $AllCertificates['MissingDNSName'].pfxPassword | Select-Object -ExpandProperty Result | should be "Warning" } It "PFX Encryption Should Pass"{ Test-PfxEncryption -pfxfile $GoodPfxEncryptionPath -pfxPassword $AllCertificates['MissingDNSName'].pfxPassword | Select-Object -ExpandProperty Result | should be "OK" } $GLOBAL:filesForCleanUp += $BadPfxEncryptionPath } } Context -Name "PaaS Certificates" { BeforeAll { Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force } # Get PaaS Certificate data $AllPaaSCertificateNames = $testdata.AppServices.Keys $AllPaaSCertificates = $testdata.AppServices $keyvaultName = $testdata.pesterdata.ptkkvName Set-SecurityProtocol -securityProtocol $targetSecurityProtocol $certificateSecrets = Get-AzureKeyVaultSecret -VaultName $keyvaultName | Where-Object Name -in $AllPaaSCertificateNames Set-SecurityProtocol -securityProtocol $restoreSecProtocol $i = 1 $pwd = New-RandomPassword $outputDir = "{0}\AppServicesPester" -f $env:TEMP Remove-Item $outputDir -Recurse -Force -ErrorAction SilentlyContinue -Verbose New-Item $outputDir -ItemType Directory -Force -Verbose foreach ($secret in $certificateSecrets) { [int]$percentageComplete = $i / $certificateSecrets.count * 100 Write-Progress -Activity "Downloading Certificates" -Status "$percentageComplete% Complete:" -PercentComplete $percentageComplete -CurrentOperation $secret.Name $destFile = "{0}\AppServicesPester\{1}\{2}.pfx" -f $env:TEMP, $secret.Name.replace('AppServices',''), $secret.Name New-Item ("{0}\{1}" -f $outputDir,$secret.Name.replace('AppServices','')) -ItemType Directory -Force -Verbose $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName $secret.Name -outputPath $destFile -asSecret:$true $AllPaaSCertificates[$secret.name].pfxPath = $pfx.pfxPath $AllPaaSCertificates[$secret.name].pfxPassword = $pfx.pfxPassword $i++ $GLOBAL:filesForCleanUp += $pfx.pfxPath } It "PaaS Certificates Should Pass" { $paasPassParam = @{ CertificateType = 'AppServices' certificatepath = "$env:temp\AppServicesPester" pfxPassword = (ConvertTo-SecureString $pwd -AsPlainText -Force) RegionName = $regionName FQDN = $externalFQDN outputPath = "$env:temp\AppServicesPester" } Invoke-AzsCertificateValidation @paasPassParam | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } It "PaaS Certificates with JSON Should Pass" { $paasJSONPassParam = @{ CertificateType = 'AppServices' certificatepath = "$env:temp\AppServicesPester" pfxPassword = (ConvertTo-SecureString $pwd -AsPlainText -Force) DeploymentDataJSONPath = $jsonPath outputPath = "$env:temp\AppServicesPester" } Invoke-AzsCertificateValidation @paasJSONPassParam | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } $customOutputPath = "{0}\PaasPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $null = new-item $customOutputPath -ItemType Directory -Force $paasJSONReportParam = @{ CertificateType = 'AppServices' certificatepath = "$env:temp\AppServicesPester" pfxPassword = (ConvertTo-SecureString $pwd -AsPlainText -Force) DeploymentDataJSONPath = $jsonPath outputPath = $customOutputPath CleanReport = $true } Invoke-AzsCertificateValidation @paasJSONReportParam [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol $reportJsonPath = Get-ChildItem $customOutputPath -Filter *.json | Select-Object -ExpandProperty Fullname $reportJSON = Get-Content $reportJsonPath | ConvertFrom-Json $GLOBAL:filesForCleanUp += $customOutputPath It "PaaS Certificates results should pass duplicate detection" { $reportJSON.CertificateValidation.AppServices.ReuseCount | Should Not BeGreaterThan 1 } It "PaaS Certificates Report should have data" { $reportJSON.CertificateValidation.AppServices.Result | Should not match 'fail' #Have all tests $testNamesEx = 'Signature Algorithm|DNS Names|Key Usage|Key Length|Parse PFX|Private Key|Cert Chain|Chain Order|Other Certificates|Expiry Date' $reportJSON.CertificateValidation.AppServices.Test | Should match $testNamesEx # All results have data $certResults = $reportJSON.CertificateValidation.AppServices foreach ($certResult in $certResults) { $certResult.Result | Should Not BeNullOrEmpty if ($certResult.Test -ne 'Parse PFX') { $certResult.FailureDetail | Should BeNullOrEmpty } $certResult.Test | Should Not BeNullOrEmpty $certResult.Path | Should Not BeNullOrEmpty $certResult.Thumbprint | Should Not BeNullOrEmpty $certResult.CertificateId | Should Not BeNullOrEmpty $certResult.ReuseCount | Should Not BeNullOrEmpty } } } Context -Name "Azure Stack Edge Certificates" { BeforeAll { Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force } # Get PaaS Certificate data $AllASECertificateNames = $testdata.AzureStackEdgeDevice.Keys $AllASECertificates = $testdata.AzureStackEdgeDevice $keyvaultName = $testdata.pesterdata.ptkkvName Set-SecurityProtocol -securityProtocol $targetSecurityProtocol $certificateSecrets = Get-AzureKeyVaultSecret -VaultName $keyvaultName | Where-Object Name -in $AllASECertificateNames Set-SecurityProtocol -securityProtocol $restoreSecProtocol $i = 1 $pwd = New-RandomPassword $outputDir = "{0}\AzureStackEdgeDevicePester" -f $env:TEMP Remove-Item $outputDir -Recurse -Force -ErrorAction SilentlyContinue -Verbose New-Item $outputDir -ItemType Directory -Force -Verbose foreach ($secret in $certificateSecrets) { [int]$percentageComplete = $i / $certificateSecrets.count * 100 Write-Progress -Activity "Downloading Certificates" -Status "$percentageComplete% Complete:" -PercentComplete $percentageComplete -CurrentOperation $secret.Name $destFile = "{0}\AzureStackEdgeDevicePester\{1}\{2}.pfx" -f $env:TEMP, $secret.Name.replace('ASE',''), $secret.Name New-Item ("{0}\{1}" -f $outputDir,$secret.Name.replace('ASE','')) -ItemType Directory -Force -Verbose $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName $secret.Name -outputPath $destFile -asSecret:$true $AllASECertificates[$secret.name].pfxPath = $pfx.pfxPath $AllASECertificates[$secret.name].pfxPassword = $pfx.pfxPassword $i++ $GLOBAL:filesForCleanUp += $pfx.pfxPath } It "Azure Stack Edge Certificates Should Pass" { $ASEPassParam = @{ CertificateType = 'AzureStackEdgeDevice' certificatepath = $outputDir DeviceName = 'DBG-KARB2NP5J' NodeSerialNumber = 'WIN-KARB2NP5J3O' pfxPassword = (ConvertTo-SecureString $pwd -AsPlainText -Force) FQDN = 'edgedomain.com' outputPath = $outputDir } Invoke-AzsCertificateValidation @ASEPassParam | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } $reportJsonPath = Get-ChildItem $outputDir -Filter *.json | Select-Object -ExpandProperty Fullname $reportJSON = Get-Content $reportJsonPath | ConvertFrom-Json $GLOBAL:filesForCleanUp += $outputDir It "Azure Stack Edge Certificates results should pass duplicate detection" { $reportJSON.CertificateValidation.AzureStackEdgeDevice.ReuseCount | Should Not BeGreaterThan 1 } It "Azure Stack Edge Certificates Report should have data" { $reportJSON.CertificateValidation.AzureStackEdgeDevice.Result | Should not match 'fail' #Have all tests $testNamesEx = 'Signature Algorithm|DNS Names|Key Usage|Key Length|Parse PFX|Private Key|Cert Chain|Chain Order|Other Certificates|Expiry Date' $reportJSON.CertificateValidation.AzureStackEdgeDevice.Test | Should match $testNamesEx # All results have data $certResults = $reportJSON.CertificateValidation.AzureStackEdgeDevice foreach ($certResult in $certResults) { $certResult.Result | Should Not BeNullOrEmpty if ($certResult.Test -ne 'Parse PFX') { $certResult.FailureDetail | Should BeNullOrEmpty } $certResult.Test | Should Not BeNullOrEmpty $certResult.Path | Should Not BeNullOrEmpty $certResult.Thumbprint | Should Not BeNullOrEmpty $certResult.CertificateId | Should Not BeNullOrEmpty $certResult.ReuseCount | Should Not BeNullOrEmpty } } } Context -Name "Deployment Certificates" { BeforeAll { Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force } $customOutputPath = "{0}\DeployCertPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $null = new-item $customOutputPath -ItemType Directory -Force $destFile = "$customOutputPath\dup.pfx" $pwd = New-RandomPassword $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName PerfectCert -outputPath $destFile -asSecret:$true $dirs = "ACSBlob", "ACSQueue", "ACSTable", "Admin Portal", "ARM Admin", "ARM Public", "KeyVault", "KeyVaultInternal", "Public Portal", "Admin Extension Host","Public Extension Host" $dirs | ForEach-Object {$dest = New-item "$customOutputPath\Certs\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } $deploymentJSONPassParam = @{ CertificateType = 'Deployment' certificatepath = "$customOutputPath\Certs" pfxPassword = $pfx.pfxPassword DeploymentDataJSONPath = $jsonPath outputPath = $customOutputPath CleanReport = $true } Invoke-AzsCertificateValidation @deploymentJSONPassParam [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol $reportJsonPath = Get-ChildItem $customOutputPath -Filter *.json | Select-Object -ExpandProperty Fullname $reportJSON = Get-Content $reportJsonPath | ConvertFrom-Json $GLOBAL:filesForCleanUp += $customOutputPath It "Deployment Certificates results should fail duplicate detection" { $reportJSON.CertificateValidation.Deployment.ReuseCount | Should BeGreaterThan 1 } It "Deployment Certificates Report should have data" { $reportJSON.CertificateValidation.Deployment.Result | Should not match 'fail' #Have all tests $testNamesEx = 'Signature Algorithm|DNS Names|Key Usage|Key Length|Parse PFX|Private Key|Cert Chain|Chain Order|Other Certificates|PFX Encryption|Expiry Date' $reportJSON.CertificateValidation.Deployment.Test | Should match $testNamesEx # All results have data $certResults = $reportJSON.CertificateValidation.Deployment foreach ($certResult in $certResults) { $certResult.Result | Should Not BeNullOrEmpty if ($certResult.Test -ne 'Parse PFX') { $certResult.FailureDetail | Should BeNullOrEmpty } $certResult.Test | Should Not BeNullOrEmpty $certResult.Path | Should Not BeNullOrEmpty $certResult.Thumbprint | Should Not BeNullOrEmpty $certResult.CertificateId | Should Not BeNullOrEmpty $certResult.ReuseCount | Should Not BeNullOrEmpty } } It "Byte array of none pfx files should fail" { $tempInvalidFile = Join-Path $env:temp "$(New-Guid).txt" $randomRubbish = Get-Random -Minimum 1 -Maximum ([int]::MaxValue) $randomRubbish | out-file -FilePath $tempInvalidFile [byte[]]$invalidByteArray = Get-Content $tempInvalidFile -Encoding Byte $invalidPassword = ConvertTo-SecureString $randomRubbish -AsPlainText -Force $params = @{ CertificateBinary = $invalidByteArray CertificatePassword = $invalidPassword ExpectedDomainFQDN = 'certificate.file' WarnOnSelfSigned = $true certConfig = @{DNSName = 'invalid';pfxPath = $tempInvalidFile} } try { Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force -DisableNameChecking $null = Test-AzSCertificate @params } catch { $_.exception.message | should match "Unable to read certificate. Does not appear to be a valid certificate. Ensure the certificate is presented as a byte array of a valid pfx file." } $GLOBAL:filesForCleanUp += $tempInvalidFile } It "Custom Single Config Passes" { # Custom Config $dest = New-item "$customOutputPath\CustomSingle" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName $customCertificateConfiguration = @{'CustomSingle' = @{ 'DNSName' = '*.table' 'EnhancedKeyUsage' = '1.3.6.1.5.5.7.3.2' } } $CustomSingleCertTestParams = @{ 'CertificateType' = 'Custom' 'CustomCertConfig' = $customCertificateConfiguration 'CertificatePath' = $dest 'RegionName' = 'east' 'ExternalFQDN' = 'azurestack.contoso.com' 'pfxPassword' = $pfx.pfxpassword 'cleanreport' = $true 'outputPath' = "$customOutputPath\CustomGroup" } Invoke-AzsCertificateValidation @CustomSingleCertTestParams | Should be $null } It "Custom Group Config Passes" { $dirs = "Custom1", "Custom2" $dirs | ForEach-Object {$dest = New-item "$customOutputPath\CustomGroup\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } $customCertificateConfiguration = @{'Custom1' = @{ 'DNSName' = '*.table' 'EnhancedKeyUsage' = '1.3.6.1.5.5.7.3.2' } 'Custom2' = @{ 'DNSName' = 'adminportal' 'KeyLength' = 2048 } } $CustomGroupCertTestParams = @{ 'CertificateType' = 'Custom' 'CustomCertConfig' = $customCertificateConfiguration 'CertificatePath' = "$customOutputPath\CustomGroup" 'RegionName' = 'east' 'ExternalFQDN' = 'azurestack.contoso.com' 'pfxPassword' = $pfx.pfxpassword 'cleanreport' = $true 'outputPath' = "$customOutputPath\CustomGroup" } Invoke-AzsCertificateValidation @CustomGroupCertTestParams | Should be $null } } Context -Name "Deploy Repo" { BeforeAll { # Deployment $deploycerts = "C:\CloudDeployment\Setup\Certificates\" $null = new-item $deploycerts\AAD -ItemType Directory -Force $null = new-item $deploycerts\ADFS -ItemType Directory -Force $pwd = New-RandomPassword $destFile = "$deploycerts\dup.pfx" $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName PerfectCert -outputPath $destFile -asSecret:$true # AAD $dirs = "ACSBlob", "ACSQueue", "ACSTable", "Admin Portal", "ARM Admin", "ARM Public", "KeyVault", "KeyVaultInternal", "Public Portal", "Admin Extension Host","Public Extension Host" $dirs | ForEach-Object {$dest = New-item "$deploycerts\AAD\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } # ADFS $dirs = "ACSBlob", "ACSQueue", "ACSTable", "ADFS", "Admin Portal", "ARM Admin", "ARM Public", "Graph", "KeyVault", "KeyVaultInternal", "Public Portal", "Admin Extension Host","Public Extension Host" $dirs | ForEach-Object {$dest = New-item "$deploycerts\ADFS\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } $srcerts = "C:\SecretRotation\" $null = new-item $srcerts\AAD -ItemType Directory -Force $null = new-item $srcerts\ADFS -ItemType Directory -Force $destFile = "$srcerts\dup.pfx" $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName PerfectCert -outputPath $destFile -asSecret:$true $GLOBAL:standalone = $false # AAD $dirs = "ACSBlob", "ACSQueue", "ACSTable", "Admin Portal", "ARM Admin", "ARM Public", "KeyVault", "KeyVaultInternal", "Public Portal", "Admin Extension Host","Public Extension Host" $dirs | ForEach-Object {$dest = New-item "$srcerts\AAD\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } # ADFS $dirs = "ACSBlob", "ACSQueue", "ACSTable", "ADFS", "Admin Portal", "ARM Admin", "ARM Public", "Graph", "KeyVault", "KeyVaultInternal", "Public Portal", "Admin Extension Host","Public Extension Host" $dirs | ForEach-Object {$dest = New-item "$srcerts\ADFS\$PSITEM" -ItemType Directory -Force; Copy-Item $pfx.pfxPath $dest.FullName } $GLOBAL:filesForCleanUp += $deploycerts $GLOBAL:filesForCleanUp += $srcerts } It "Test-AzureStackCerts should succeed for AAD deployment" { Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force $azureStackCertsResult = Test-AzureStackCerts -ExpectedDomainFQDN east.azurestack.contoso.com -CertificatePassword $pfx.pfxpassword -UseADFS $false | Select-Object -ExpandProperty Result | Should match 'OK' } It "Test-AzureStackCerts should succeed for ADFS deployment" { Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force $azureStackCertsResult = Test-AzureStackCerts -ExpectedDomainFQDN east.azurestack.contoso.com -CertificatePassword $pfx.pfxpassword -UseADFS $true | Select-Object -ExpandProperty Result | Should match 'OK' } It "Test-AzureStackCerts should succeed for AAD Secret Rotation" { Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force $testCertsParams = @{ CertificatePassword = $pfx.pfxpassword ExpectedDomainFQDN = 'east.azurestack.contoso.com' PfxFilesPath = $srcerts UseADFS = $false } $azureStackCertsResult = Test-AzureStackCerts @testCertsParams | Select-Object -ExpandProperty Result | Should match 'OK' } It "Test-AzureStackCerts should succeed for ADFS Secret Rotation" { Import-Module ..\CertificateValidation\PublicCertHelper.psm1 -Force $testCertsParams = @{ CertificatePassword = $pfx.pfxpassword ExpectedDomainFQDN = 'east.azurestack.contoso.com' PfxFilesPath = $srcerts UseADFS = $true } $azureStackCertsResult = Test-AzureStackCerts @testCertsParams | Select-Object -ExpandProperty Result | Should match 'OK' } } } Describe "Certificate Generation" { Context "Hardware Certificate Generation "{ $command = get-command New-SelfSignedHardwareCertificate $CertificateTypes = $command.Parameters.CertificateType.Attributes.ValidValues foreach ($certificateType in $CertificateTypes) { It "$certificateType Should Generate Self Signed Certificate in all formats" { $customOutputPath = "{0}\hwcertPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath $pfxPassword = ConvertTo-SecureString -String (New-RandomPassword) -AsPlainText -Force $newCertOutput = New-SelfSignedHardwareCertificate -CertificateType $certificateType -DnsRecord "NodeName-01" -pfxPassword $pfxPassword -OutputPath $customOutputPath Get-Item $newCertOutput | Should Not BeNullOrEmpty #check conversion to key/pem $passthrucert = New-SelfSignedHardwareCertificate -CertificateType $certificateType -DnsRecord "PassThru-01" -pfxPassword $pfxPassword -OutputPath $customOutputPath -passthru $pemDir = ConvertTo-PEM -certificate $passthrucert -path $customOutputPath Get-ChildItem $pemDir | where-object {$_.Name -eq "$($passThruCert.Thumbprint).pem"} | Should Not BeNullOrEmpty Get-ChildItem $pemDir | where-object {$_.Name -eq "$($passThruCert.Thumbprint).key"} | Should Not BeNullOrEmpty } } } Context "DistinguishedName Parameter Combinations" { Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force $command = get-command New-AzsCertificateSigningRequest $requestTypes = $command.Parameters.requestType.Attributes.ValidValues $IdentitySystems = $command.Parameters.IdentitySystem.Attributes.ValidValues $CertificateTypes = $command.Parameters.CertificateType.Attributes.ValidValues foreach ($certificateType in $CertificateTypes) { foreach ($IdentitySystem in $IdentitySystems) { foreach ($requestType in $requestTypes) { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = $CertificateType RegionName = 'east' ExternalFQDN = "$IdentitySystem.$requestType.$certificateType.com" DistinguishedName = $subject RequestType = $requestType IdentitySystem = $IdentitySystem OutputRequestPath = $customOutputPath OutputPath = $customOutputPath } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath It ("{0} {1} {2}" -f $CertificateType, $IdentitySystem, $requestType) { try { New-AzsCertificateSigningRequest @params | should be $null } catch { $_ | should be $null } } } } if ($certificateType -eq 'Deployment') { It "User should use IdentitySystem with Deployment certificate requests" { $params.Remove('IdentitySystem') try { New-AzsCertificateSigningRequest @params } catch { $_ | should contain 'IdentitySystem not provided' } } } } } Context "AzureStack Edge Request Generation" { It "AzureStack Edge Device MultipleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeDevice' DeviceName = 'DBG-KARB2NP5J' NodeSerialNumber = 'WIN-KARB2NP5J3O' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge Device SingleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeDevice' DeviceName = 'DBG-KARB2NP5J' NodeSerialNumber = 'WIN-KARB2NP5J3O' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" RequestType = 'SingleCSR' } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge VPN MultipleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeVPN' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge VPN SingleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeVPN' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" RequestType = 'SingleCSR' } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge Wifi Client MultipleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeWifiClient' NodeSerialNumber = 'WIN-KARB2NP5J3O' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge Wifi Client SingleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeWifiClient' NodeSerialNumber = 'WIN-KARB2NP5J3O' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" RequestType = 'SingleCSR' } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge Wifi Server MultipleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeWifiServer' RadiusServerName = 'WIN-RadiusServer' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "AzureStack Edge Wifi Server SingleCSR Request should complete" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack Edge,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AzureStackEdgeWifiServer' RadiusServerName = 'WIN-RadiusServer' externalFQDN = 'AzureStackEdgeDevice.contoso.com' OutputRequestPath = "$ENV:USERPROFILE\Documents\AzsCertRequests" RequestType = 'SingleCSR' } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } } Context "Subject Variants" { It "Simple Subject - No common name" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Azure Stack,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AppServices' RegionName = 'noCN' ExternalFQDN = 'subject.variants.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "Double OUs" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'OU=Double,OU=OUs,O=HumongousInsurance,L=Pester-On-Puddle,ST=Pestershire,C=UK' $params = @{ CertificateType = 'AppServices' RegionName = 'east' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "Subject values contain commas, with flag" { Import-Module $PSScriptRoot\..\CertificateValidation\Microsoft.AzureStack.PublicCertificateRequest.psm1 -Force $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'CN=Comma Values with flag;OU="Azure,Stack";O=HumongousInsurance;L=Pester-On-Puddle;ST=Pestershire;C=UK' $params = @{ CertificateType = 'AppServices' RegionName = 'commaflag' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject DistinguishedNameflag = [System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::UseSemiColons RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath try { New-AzsCertificateSigningRequest @params } catch { $_ | should be $null } } It "Subject values contain commas, with no flag should fail" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = 'CN="Comma Values";OU="Azure Stack";O="Humongous,Insurance";L="Pester-On-Puddle";ST="Pestershire";C="UK"' $params = @{ CertificateType = 'AppServices' RegionName = 'commanoflag' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath New-AzsCertificateSigningRequest @params Get-ChildItem -Path $customOutputPath -filter '*.req' | should be $null } } Context "Attribute Handling" { $KeyLengths = $command.Parameters.KeyLength.Attributes.ValidValues Foreach ($KeyLength in $KeyLengths) { It "KeyLength $KeyLength should be requested" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = "CN=KeyLength $KeyLength,OU=Azure Stack,O=Humongous Insurance,L=Pester-On-Puddle,ST=Pestershire,C=UK" $params = @{ CertificateType = 'AppServices' RegionName = 'keylength' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath KeyLength = $KeyLength } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath New-AzsCertificateSigningRequest @params $infFiles = Get-ChildItem -Path $customOutputPath -filter '*.inf' -Recurse foreach ($infFile in $infFiles.fullname) { get-content "$infFile" | Select-String -Pattern "KeyLength = $KeyLength" | should be $true } } } $HashAlgorithms = 'SHA256', 'SHA384', 'SHA512' Foreach ($HashAlgorithm in $HashAlgorithms) { It "HashAlgorithm $HashAlgorithm should be requested" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = "CN=HashAlgorithm $HashAlgorithm,OU=Azure Stack,O=Humongous Insurance,L=Pester-On-Puddle,ST=Pestershire,C=UK" $params = @{ CertificateType = 'AppServices' RegionName = 'HashAlgorithm' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath HashAlgorithm = $HashAlgorithm } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath New-AzsCertificateSigningRequest @params $infFiles = Get-ChildItem -Path $customOutputPath -filter '*.inf' -Recurse foreach ($infFile in $infFiles.fullname) { get-content "$infFile" | Select-String -Pattern "HashAlgorithm = $HashAlgorithm" | should be $true } } } It "Enhanced Key Usage should be requested" { $customOutputPath = "{0}\csrPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $subject = "CN=Enhanced Key Usage,OU=Azure Stack,O=Humongous Insurance,L=Pester-On-Puddle,ST=Pestershire,C=UK" $params = @{ CertificateType = 'AppServices' RegionName = 'EnhancedKeyUsage' ExternalFQDN = 'azurestack.HumongousInsurance.com' DistinguishedName = $subject RequestType = 'MultipleCSR' IdentitySystem = 'AAD' OutputRequestPath = $customOutputPath OutputPath = $customOutputPath EnhancedKeyUsage = @('Server Authentication',@{'Custom Usage' = '1.3.6.1.5.7.8.2.1'}) } $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath New-AzsCertificateSigningRequest @params $infFiles = Get-ChildItem -Path $customOutputPath -filter '*.inf' -Recurse foreach ($infFile in $infFiles.fullname) { get-content "$infFile" | Select-String -Pattern "%szOID_Custom_Usage%" | should be $true get-content "$infFile" | Select-String -Pattern 'szOID_Custom_Usage = "1.3.6.1.5.7.8.2.1"' | should be $true } } } AfterAll { $GLOBAL:filesForCleanUp += Get-ChildItem -Path Cert:\LocalMachine\REQUEST | Where-Object { $_.Subject -match 'Pester-On-Puddle' } } } Describe -Name "Certificate Repair" { BeforeAll{ Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force $regionName = $deploymentDataJSON.DeploymentData.RegionName $externalFQDN = $deploymentDataJSON.DeploymentData.ExternalDomainFQDN } Context "Certificate ImportExport" { It "Certificate Import/Export Should Pass" { $certName = 'BadPrivateKey' $destfile = "$ENV:TEMP\ImportExportPreCert.pfx" $pwd = New-RandomPassword $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName $certName -outputPath $destFile -asSecret:$true $exportPFXPath = "$ENV:TEMP\ImportExportPesterCertificate.pfx" Repair-AzsPfxCertificate -pfxPath $pfx.pfxPath -pfxPassword $pfx.pfxPassword -exportPFXPath $exportPFXPath | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol $GLOBAL:filesForCleanUp += $exportPFXPath $GLOBAL:filesForCleanUp += $destfile } } } Describe -Name "Azure Registration"{ BeforeAll{ Connect-Azure Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force $registrationUsername = $testdata.Registration.AccountName $registrationPassword = $testdata.Registration.Password $registrationSubscriptionName = $testdata.Registration.RegistrationSubscriptionID $AzureEnvironment = $testdata.Registration.AzureEnvironment $registrationSubscriptionID = Get-PlainTextSecret -VaultName $keyvaultName -secretName $registrationSubscriptionName $registrationCredential = Get-CredentialSecret -VaultName $keyvaultName -username $registrationUsername -password $registrationPassword } Context -Name "Azure Registration" { It "Azure Registration with JSON Should Pass" { Invoke-AzsRegistrationValidation -RegistrationAccount $registrationCredential -deploymentDataJSONPath $jsonPath -RegistrationSubscriptionID $registrationSubscriptionID -outputPath "$ENV:TEMP\PesterRegChecks" -CleanReport | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol $json = Get-Content $ENV:TEMP\PesterRegChecks\AzsReadinessCheckerReport.json | ConvertFrom-Json $json.AzureValidation.AzureRegistration.Result | Should Be 'OK' $json.AzureValidation.AzureRegistration.Assets | Should Not BeNullOrEmpty $json.AzureValidation.AzureRegistration.Test | Should Be 'RegistrationAccount' $json.AzureValidation.AzureRegistration.ErrorDetails | Should BeNullOrEmpty $json.AzureValidation.AzureRegistration.SubscriptionDetail | Should Not BeNullOrEmpty $json.AzureValidation.AzureRegistration.SubscriptionDetail.subscription | Should Not BeNullOrEmpty $json.AzureValidation.AzureRegistration.SubscriptionDetail.subscription.subscriptionPolicies | Should Not BeNullOrEmpty } It "Azure Registration Check Should Pass" { Invoke-AzsRegistrationValidation -RegistrationAccount $registrationCredential -AzureEnvironment $AzureEnvironment -RegistrationSubscriptionID $registrationSubscriptionID -CleanReport | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } It "Azure Registration Check with custom AzureEnvironment Should Pass" { Invoke-AzsRegistrationValidation -RegistrationAccount $registrationCredential -CustomCloudARMEndpoint $CustomAzureEnvironmentUri -RegistrationSubscriptionID $registrationSubscriptionID -CleanReport | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } } AfterAll{ $GLOBAL:filesForCleanUp += "$ENV:TEMP\PesterRegChecks" } } Describe -Name "Azure Identity" { BeforeAll { Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force Connect-Azure foreach ($key in $testdata.Identity.Keys) { Write-Verbose ("$key : Username: {0} password: {1}" -f $testData.Identity[$key].AccountName, $testData.Identity[$key].Password) $testData.Identity[$key].Credential = Get-CredentialSecret ` -VaultName $keyvaultName ` -username $testData.Identity[$key].AccountName ` -password $testData.Identity[$key].Password } } Context -Name "Azure Identity" { $Account = $testdata.Identity.GlobalAdmin It "Azure Identity with JSON Should Pass" { Invoke-AzsAzureIdentityValidation -AADServiceAdministrator $Account.Credential -deploymentDataJSONPath $jsonPath -CleanReport -outputPath "$ENV:TEMP\PesterIdentityChecks" | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol $json = Get-Content $ENV:TEMP\PesterIdentityChecks\AzsReadinessCheckerReport.json | ConvertFrom-Json $json.AzureValidation.AzureInstallResult.Result | Should Be 'OK' $json.AzureValidation.AzureInstallResult.Assets | Should Not BeNullOrEmpty $json.AzureValidation.AzureInstallResult.Test | Should Be 'ServiceAdministrator' $json.AzureValidation.AzureInstallResult.ErrorDetails | Should BeNullOrEmpty } $GLOBAL:filesForCleanUp += "$ENV:TEMP\PesterIdentityChecks" It "Azure Identity Should Pass" { Invoke-AzsAzureIdentityValidation -AADServiceAdministrator $Account.Credential -AzureEnvironment AzureCloud -AADDirectoryTenantName $Account.DirectoryName | Should Be $null [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } } Context -Name "Azure Identity Units" { Set-SecurityProtocol -securityProtocol $targetSecurityProtocol Import-Module ..\AzureValidation\Microsoft.AzureStack.AzureValidation.Internal.psm1 -Force -DisableNameChecking Import-Module ..\Microsoft.AzureStack.ReadinessChecker.Reporting.psm1 -Force It "CustomAzureEnvironment should success" { $Account = $testdata.Identity.GlobalAdmin # injecting global variable for custom config file, which we usually be supplied by the user $global:CustomAzureEnvironmentJSON = $CustomAzureEnvironmentUri $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment CustomCloud -CustomCloudARMEndpoint $CustomAzureEnvironmentUri $result.result | should be "OK" $result.errorDetails | should be $null } It "AzureChinaCloud should fail" { $Account = $testdata.Identity.GlobalAdmin $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureChinaCloud $result.result | should be "Fail" $result.errorDetails | should match 'Unknown user type detected' } It "AzureGermanCloud should fail" { $Account = $testdata.Identity.GlobalAdmin $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureGermanCloud $result.result | should be "Fail" $result.errorDetails | should match 'Unknown user type detected' } It "AzureUSGovernment should succeed" { $Account = $testdata.Identity.GlobalAdmin $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureUSGovernment $result.result | should be "OK" $result.errorDetails | should be $null } It "AzureCloud should succeed" { $Account = $testdata.Identity.GlobalAdmin $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureCloud $result.result | should be "OK" $result.errorDetails | should be $null } It "Live Accounts should succeed" { $Account = $testdata.Identity.Live $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.Credential.Username.split('@')[1] -AzureEnvironment AzureCloud $result.result | should be "OK" $result.errorDetails | should be $null } It "Disabled Accounts should Fail" { $Account = $testdata.Identity.Disabled $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureCloud $result.result | should be "Fail" $result.errorDetails | should match 'User account is disabled' } It "Temp Password Accounts should Fail" { $Account = $testdata.Identity.TempPassword $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureCloud $result.result | should be "Fail" $result.errorDetails | should match 'expired or is a temporary password that needs to be reset before continuing' } if ($TestMFA) { It "MFA Accounts should succeed" { $Account = $testdata.Identity.MFA $result = Test-AzsServiceAdministrator -AADServiceAdministrator $Account.Credential -AADDirectoryTenantName $Account.DirectoryName -AzureEnvironment AzureCloud $result.result | should be "OK" $result.errorDetails | should be $null } } else { Write-Warning -Message "MFA testing was skipped." } } AfterAll{ Set-SecurityProtocol -securityProtocol $RestoreSecProtocol Get-Module Microsoft.AzureStack.AzureValidation.Internal.psm1 | Remove-Module } } Describe -Name "Password Tests" { #Connect to Azure BeforeAll{ Connect-Azure Import-Module ..\Microsoft.AzureStack.ReadinessChecker.psd1 -Force $BadPasswordLength = $testdata.passwords.BadPasswordLength $BadPasswordComplexity = $testdata.passwords.BadPasswordComplexity $BadPasswordLengthSecret = ConvertTo-SecureString -string (Get-PlainTextSecret -VaultName $keyvaultName -secretName $BadPasswordLength) -AsPlainText -Force $BadPasswordComplexitySecret = ConvertTo-SecureString -string (Get-PlainTextSecret -VaultName $keyvaultName -secretName $BadPasswordComplexity) -AsPlainText -Force $customOutputPath = "{0}\DeployCertPester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $null = new-item $customOutputPath -ItemType Directory -Force $destFile = "$customOutputPath\badpassword.pfx" $pwd = New-RandomPassword $pfx = Save-PFXData -pwd $pwd -VaultName $keyvaultName -CertName PerfectCert -outputPath $destFile -asSecret:$true } Context -Name "PfxPassword Parameter" { It "Parameter PfxPassword should fail bad Password Length" { try { Repair-AzsPfxCertificate -pfxPath $pfx.pfxPath -pfxPassword $BadPasswordLengthSecret -exportPFXPath $env:TEMP\shouldneverexist.pfx } catch { $_.exception.message | should match "Password length cannot be fewer than" } [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } It "Parameter PfxPassword should fail bad Password Complexity" { try { Repair-AzsPfxCertificate -pfxPath $pfx.pfxPath -pfxPassword $BadPasswordComplexitySecret -exportPFXPath $env:TEMP\shouldneverexist.pfx } catch { $_.exception.message | should match "Password does not meet complexity requirements" } [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } } Context -Name "PaaSCertificates" { #$PaaSCertBadPassLgth = @{'PaaSDBCert' = @{'pfxPath' = $pfx.pfxPath; 'pfxPassword' = $BadPasswordLengthSecret}} $PaaSCertBadPassLgth = @{ CertificateType = 'AppServices' certificatepath = "$env:temp\AppServicesPester" pfxPassword = $BadPasswordLengthSecret RegionName = $regionName FQDN = $externalFQDN outputPath = "$env:temp\AppServicesPester" } It "Parameter PaaSCertificates should fail bad Password Length" { try { Invoke-AzsCertificateValidation @PaaSCertBadPassLgth } catch { $_.exception.message | should match "Password length cannot be fewer than" } [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } #$PaaSCertBadPassCplx = @{'PaaSDBCert' = @{'pfxPath' = $pfx.pfxPath; 'pfxPassword' = $BadPasswordComplexitySecret}} $PaaSCertBadPassCplx = @{ CertificateType = 'AppServices' certificatepath = "$env:temp\AppServicesPester" pfxPassword = $BadPasswordComplexitySecret RegionName = 'east' FQDN = 'azurestack.contoso.com' outputPath = "$env:temp\AppServicesPester" } It "Parameter PaaSCertificates should fail bad Password Complexity" { try { Invoke-AzsCertificateValidation @PaaSCertBadPassCplx } catch { $_.exception.message | should match "Password does not meet complexity requirements." } [Net.ServicePointManager]::SecurityProtocol | Should be $RestoreSecProtocol } } AfterAll{ $GLOBAL:filesForCleanUp += $customOutputPath $GLOBAL:filesForCleanUp += "$env:temp\AppServicesPester" Get-Module Microsoft.AzureStack.ReadinessChecker | Remove-Module Connect-Azure } } Describe -Name "Certificate Folders" { BeforeAll { Import-Module $PSScriptRoot\..\Microsoft.AzureStack.ReadinessChecker.psd1 -force Import-Module $PSScriptRoot\..\CertificateValidation\Microsoft.AzureStack.CertificateValidation.psm1 -force } Context "Cetificate Folder Structure"{ $command = get-command New-AzsCertificateFolder $CertificateTypes = $command.Parameters.CertificateType.Attributes.ValidValues $certificateConfigDataFile = Import-PowerShellDataFile -Path $PSScriptRoot\..\CertificateValidation\Microsoft.AzureStack.CertificateConfig.psd1 foreach ($certificateType in $CertificateTypes) { $certificateConfig = $certificateConfigDataFile.CertificateTypes[$CertificateType] It "$certificateType Should Generate Correct Folder structure" { $customOutputPath = "{0}\FolderStructurePester{1}" -f $ENV:TEMP, (Get-Date -f 'yyyyMMddHHmmss') $null = New-Item -Path $customOutputPath -ItemType Directory -Force $GLOBAL:filesForCleanUp += $customOutputPath $CertFolderOutput = New-AzsCertificateFolder -certificateType $certificateType -IdentitySystem ADFS -OutputPath $customOutputPath #$CertFolderOutput | Should be $true $CertFolderOutput | ForEach-Object {New-Item -Path $PSITEM.FullName -Name 'pester.pfx' -ItemType File} try { Test-AzsCertificatePlacement -UseADFS:$false -certificatePath (Join-Path $customOutputPath $certificateType) -certConfig $certificateConfig } catch { $TestPlacementError = $_ } $TestPlacementError | Should Be $null } } } } if (-not $keepLogs) { Write-Verbose ("Removing {0}" -f ($filesForCleanUp -join ',')) -Verbose Remove-TestFiles -in "$ENV:TEMP\AzsReadinessChecker" Remove-TestFiles -in $filesForCleanUp Get-ChildItem ..\..\AzsReadinessChecker -Recurse | Where-Object extension -in '.log', '.xml', '.json' | Remove-Item -Force } else { Write-Verbose "Log clean up skipped for troubleshooting, to remove logs run the following" -Verbose } # SIG # Begin signature block # MIIjigYJKoZIhvcNAQcCoIIjezCCI3cCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDoE6c3yxTMlDcL # KXopklkzY0Ieu4ouInmOO3oNIfxUeaCCDYUwggYDMIID66ADAgECAhMzAAABUptA # n1BWmXWIAAAAAAFSMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMTkwNTAyMjEzNzQ2WhcNMjAwNTAyMjEzNzQ2WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQCxp4nT9qfu9O10iJyewYXHlN+WEh79Noor9nhM6enUNbCbhX9vS+8c/3eIVazS # YnVBTqLzW7xWN1bCcItDbsEzKEE2BswSun7J9xCaLwcGHKFr+qWUlz7hh9RcmjYS # kOGNybOfrgj3sm0DStoK8ljwEyUVeRfMHx9E/7Ca/OEq2cXBT3L0fVnlEkfal310 # EFCLDo2BrE35NGRjG+/nnZiqKqEh5lWNk33JV8/I0fIcUKrLEmUGrv0CgC7w2cjm # bBhBIJ+0KzSnSWingXol/3iUdBBy4QQNH767kYGunJeY08RjHMIgjJCdAoEM+2mX # v1phaV7j+M3dNzZ/cdsz3oDfAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU3f8Aw1sW72WcJ2bo/QSYGzVrRYcw # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ1NDEzNjAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AJTwROaHvogXgixWjyjvLfiRgqI2QK8GoG23eqAgNjX7V/WdUWBbs0aIC3k49cd0 # zdq+JJImixcX6UOTpz2LZPFSh23l0/Mo35wG7JXUxgO0U+5drbQht5xoMl1n7/TQ # 4iKcmAYSAPxTq5lFnoV2+fAeljVA7O43szjs7LR09D0wFHwzZco/iE8Hlakl23ZT # 7FnB5AfU2hwfv87y3q3a5qFiugSykILpK0/vqnlEVB0KAdQVzYULQ/U4eFEjnis3 # Js9UrAvtIhIs26445Rj3UP6U4GgOjgQonlRA+mDlsh78wFSGbASIvK+fkONUhvj8 # B8ZHNn4TFfnct+a0ZueY4f6aRPxr8beNSUKn7QW/FQmn422bE7KfnqWncsH7vbNh # G929prVHPsaa7J22i9wyHj7m0oATXJ+YjfyoEAtd5/NyIYaE4Uu0j1EhuYUo5VaJ # JnMaTER0qX8+/YZRWrFN/heps41XNVjiAawpbAa0fUa3R9RNBjPiBnM0gvNPorM4 # dsV2VJ8GluIQOrJlOvuCrOYDGirGnadOmQ21wPBoGFCWpK56PxzliKsy5NNmAXcE # x7Qb9vUjY1WlYtrdwOXTpxN4slzIht69BaZlLIjLVWwqIfuNrhHKNDM9K+v7vgrI # bf7l5/665g0gjQCDCN6Q5sxuttTAEKtJeS/pkpI+DbZ/MIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCFVswghVXAgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAFSm0CfUFaZdYgAAAAA # AVIwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJHW # rG9fi4NDdb3pPuY5SdZdPQE7RH0JF1uw6eJ9GFeAMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAN+Knp0LAafLWwmJu0va37pw1A2g6JKlF63xe # cxBSfQ+Pf0ObG/SzviGHBdFa0SUo3+SDdCvpGoyegV/TsVlDM9E+CEq3orbz2nPl # bYc2D/i9i7HDOGF4AYo86FEu5hQC8RBm0CuPIq2ue8QXgI+hVFI2/m9Izf6SGfE7 # KVaUxgNy2jo7lhc0/uz98s2hOqyuM3pEopRYS2NZLe20xEA9o7raRmtx2rP5v4Ul # vOVFs15dq6Rl13I3e0MSsGfwV1abUbB+1Ppikpmrmshv7oEeEzCbJbRSOP/JV/Kc # 7hBvN5NB1D+V3KwGph6UGmuWVoYY0tVh3W/5nchy/Ms3rBi4zaGCEuUwghLhBgor # BgEEAYI3AwMBMYIS0TCCEs0GCSqGSIb3DQEHAqCCEr4wghK6AgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCAkUHCNraro2ZA4cZe5TYDeEAMA4JvoKqDF # U07SktknDgIGXioHxEEDGBMyMDIwMDIxMjIyMTAxMy42OTRaMASAAgH0oIHQpIHN # MIHKMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQL # ExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMg # VFNTIEVTTjpBRTJDLUUzMkItMUFGQzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZaCCDjwwggTxMIID2aADAgECAhMzAAABFpMi6r+7LU3mAAAA # AAEWMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # MB4XDTE5MTExMzIxNDAzNFoXDTIxMDIxMTIxNDAzNFowgcoxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVy # aWNhIE9wZXJhdGlvbnMxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkFFMkMtRTMy # Qi0xQUZDMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIB # IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Pgb8/296ie/Lj2rWq+MIlMZ # wkSUwZsIKd472tyeVOyNcKgqSCT4zQvz2kd+VD7lYWN3V0USL5oipdp+xp7wH7CA # HC7zNU21PjdHWPOi2okIlPyTikrQBowo+MOV9Xgd3WqMnJSKEank7QmSHgJimJ2q # /ZRR5+0Z5uZRejJHkQcJmTB8Gq/wg2E/gjuRl/iGa4fGJu0cHSUiX78m5FEyaac1 # XnkqafSqYR8qb7sn3ZVt/ltbiGUJr874oi2bZduUtCMR0QiWWfBMExcLV4A6ermC # 98cbbvi/pQb1p1l7vXT2NReD+xkFqzKn0cA3Vi9cc5LjDhY91L18RuHIgU3qHQID # AQABo4IBGzCCARcwHQYDVR0OBBYEFOW/Xiu4F+gXzUflH3k0/lfIIVULMB8GA1Ud # IwQYMBaAFNVjOlyKMZDzQ3t8RhvFM2hahW1VMFYGA1UdHwRPME0wS6BJoEeGRWh0 # dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1RpbVN0 # YVBDQV8yMDEwLTA3LTAxLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKG # Pmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljVGltU3RhUENB # XzIwMTAtMDctMDEuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUH # AwgwDQYJKoZIhvcNAQELBQADggEBADaDatfaqaPbAy/pSdK8e8XdzN6v9979NSWL # UsNHoNBFpyr1FTGcvwf0SKIfe0ygt8s8plkAYxMUftUmOnO+OnGXUgTOreXIw4zt # sepotreHcL094+bn7OUGLPMa56GQii3WUgiGPP0gfNXhXcqSdd9HmXjMhKfRn0jO # KREJTPqPHLXSxcA1SVTrg8JDtkD+yWVzuuAkSopTGxtJp5PcrYUrMb7nW1coIe7t # sQiSPp6xFVzKfXFUJ9VzAChucE+8pqXLpV/xU3p/1vf0DgLZMpI22mwAgbe/E6wg # yDSKyHXI4UsiIlSYASv+IlKOtcXzrXV0IRQUdRyIC1ZiWWL/YggwggZxMIIEWaAD # AgECAgphCYEqAAAAAAACMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBD # ZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0xMDA3MDEyMTM2NTVaFw0yNTA3 # MDEyMTQ2NTVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw # DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x # JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIIBIjANBgkq # hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqR0NvHcRijog7PwTl/X6f2mUa3RUENWl # CgCChfvtfGhLLF/Fw+Vhwna3PmYrW/AVUycEMR9BGxqVHc4JE458YTBZsTBED/Fg # iIRUQwzXTbg4CLNC3ZOs1nMwVyaCo0UN0Or1R4HNvyRgMlhgRvJYR4YyhB50YWeR # X4FUsc+TTJLBxKZd0WETbijGGvmGgLvfYfxGwScdJGcSchohiq9LZIlQYrFd/Xcf # PfBXday9ikJNQFHRD5wGPmd/9WbAA5ZEfu/QS/1u5ZrKsajyeioKMfDaTgaRtogI # Neh4HLDpmc085y9Euqf03GS9pAHBIAmTeM38vMDJRF1eFpwBBU8iTQIDAQABo4IB # 5jCCAeIwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNVjOlyKMZDzQ3t8RhvF # M2hahW1VMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAP # BgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjE # MFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kv # Y3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEF # BQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w # a2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MIGgBgNVHSABAf8E # gZUwgZIwgY8GCSsGAQQBgjcuAzCBgTA9BggrBgEFBQcCARYxaHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL1BLSS9kb2NzL0NQUy9kZWZhdWx0Lmh0bTBABggrBgEFBQcC # AjA0HjIgHQBMAGUAZwBhAGwAXwBQAG8AbABpAGMAeQBfAFMAdABhAHQAZQBtAGUA # bgB0AC4gHTANBgkqhkiG9w0BAQsFAAOCAgEAB+aIUQ3ixuCYP4FxAz2do6Ehb7Pr # psz1Mb7PBeKp/vpXbRkws8LFZslq3/Xn8Hi9x6ieJeP5vO1rVFcIK1GCRBL7uVOM # zPRgEop2zEBAQZvcXBf/XPleFzWYJFZLdO9CEMivv3/Gf/I3fVo/HPKZeUqRUgCv # OA8X9S95gWXZqbVr5MfO9sp6AG9LMEQkIjzP7QOllo9ZKby2/QThcJ8ySif9Va8v # /rbljjO7Yl+a21dA6fHOmWaQjP9qYn/dxUoLkSbiOewZSnFjnXshbcOco6I8+n99 # lmqQeKZt0uGc+R38ONiU9MalCpaGpL2eGq4EQoO4tYCbIjggtSXlZOz39L9+Y1kl # D3ouOVd2onGqBooPiRa6YacRy5rYDkeagMXQzafQ732D8OE7cQnfXXSYIghh2rBQ # Hm+98eEA3+cxB6STOvdlR3jo+KhIq/fecn5ha293qYHLpwmsObvsxsvYgrRyzR30 # uIUBHoD7G4kqVDmyW9rIDVWZeodzOwjmmC3qjeAzLhIp9cAvVCch98isTtoouLGp # 25ayp0Kiyc8ZQU3ghvkqmqMRZjDTu3QyS99je/WZii8bxyGvWbWu3EQ8l1Bx16HS # xVXjad5XwdHeMMD9zOZN+w2/XU/pnR4ZOC+8z1gFLu8NoFA12u8JJxzVs341Hgi6 # 2jbb01+P3nSISRKhggLOMIICNwIBATCB+KGB0KSBzTCByjELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJp # Y2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046QUUyQy1FMzJC # LTFBRkMxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoB # ATAHBgUrDgMCGgMVAIdNW9zyT6CLG1qCDNc++szs3ZZDoIGDMIGApH4wfDELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9z # b2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDh7uQAMCIY # DzIwMjAwMjEzMDQ1MjE2WhgPMjAyMDAyMTQwNDUyMTZaMHcwPQYKKwYBBAGEWQoE # ATEvMC0wCgIFAOHu5AACAQAwCgIBAAICFskCAf8wBwIBAAICEsQwCgIFAOHwNYAC # AQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEK # MAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQBXj5lDfJGsYt9IEhaxj3jtB/C4 # zGk4+/nSmi1s/DktHFLd9FEWO3v2VcCqYXs15VvQkd1wGv8bjQroGa6W6l7ZIlRO # 5l1m9YAFjL1xoNnThzJKUk0fM4EMro0ebkkmCisBAabyf6RVPIFVcMs5vLGym3Rm # k+lDHQlc1eSzuy0jkjGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwAhMzAAABFpMi6r+7LU3mAAAAAAEWMA0GCWCGSAFlAwQCAQUAoIIB # SjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIM4K # e11ONKvs8hp+rtEMjYvhXgFewZ2sLARvO3Mq6aUGMIH6BgsqhkiG9w0BCRACLzGB # 6jCB5zCB5DCBvQQggyKU9qRgKQiXXCmbITbdtLENhYxqIMhBaM+iXtLBkMowgZgw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAARaTIuq/uy1N # 5gAAAAABFjAiBCDKk0wBInYz+a+1/B/VLAinR118bkU6wbb+lzGbmzqhTTANBgkq # hkiG9w0BAQsFAASCAQAbB6Zaxz9uiLAn6v1Yn7a6cxQfyrWZx6MUK9vldKP3uw9U # JxpyCSgXsoUgff03LQyBVzXPS/io+ut+aY+PQKK+Ubsy4h3jCsrzuxOsHx8N7/+u # c8y5N1RC2Qd1pcr2RjNd551YvUuHq3fXfMeaNqr2ZEDAjE4hc+tv9HOib5eoZ/1s # Oo33/QwG8E8XO8mo4r/clJQ5BtRGRcCTYTTlWE6o9yQVhC6m40wJERVFtuIwnmn1 # l4Jsfq+hzuydZXpI9Cfte/0rqLTm0GM04l/ADVgv8GqFuu9ctjVvjT7cAvVfJMHO # ScN7wC8Cx5z4e/CIX9OqKUaAkylsDHzh6QhguiBN # SIG # End signature block |