X509CertHelper.ps1
<#PSScriptInfo .VERSION 1.0 .GUID c930fda0-ffbe-4179-b24c-f34e40f34903 .AUTHOR alain .COMPANYNAME alainQtec .COPYRIGHT Copyright © 2022 alain. All rights reserved. .TAGS x509, PKI .LICENSEURI https://alain.mit-license.org/ .PROJECTURI https://gist.github.com/alainQtec/d8f277f1d830882c4927c144a99b70cd .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES customized to my use case .PRIVATEDATA #> <# .SYNOPSIS A custom Powershell class to help with X509Certificate2 operations .DESCRIPTION A Powershell class to perform X509Certificate2 related functions without relying on PKI cmdlets because the PKI module is not pre-installed on all OSs (Ex: On Arch Linux). .EXAMPLE . ([scriptblock]::Create($((Invoke-RestMethod -Method Get https://api.github.com/gists/d8f277f1d830882c4927c144a99b70cd).files.'X509CertHelper.ps1'.content))); $Certificate = [X509CertHelper]::CreateSelfSignedCertificate('Test_Cert'); This examples how to quickly create an X509Certificate2 using X509CertHelper class. .LINK https://gist.github.com/alainQtec/d8f277f1d830882c4927c144a99b70cd .NOTES Author : Alain Herve License : MIT #> enum ECCurveName { ansix9p256r1 ansix9p384r1 ansix9p521r1 brainpoolP256r1 brainpoolP384r1 brainpoolP512r1 nistP256 nistP384 nistP521 secp256k1 } class X509CertHelper { X509CertHelper() {} static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([string]$subjectName) { return [X509CertHelper]::CreateSelfSignedCertificate($subjectName, [ECCurveName]::nistP521); } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([string]$subjectName, [ECCurveName]$curveName) { [void][x509CertHelper]::IsValidDistinguishedName($subjectName, $true); $ecdsa = [System.Security.Cryptography.ECDsa]::Create(); $ecdsa.GenerateKey([System.Security.Cryptography.ECCurve]::CreateFromFriendlyName("$curveName")); $certRequest = [X509CertHelper]::GetCertificateRequest($subjectName, $ecdsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256, 2048) return [X509CertHelper]::CreateSelfSignedCertificate($certRequest, [IO.FileInfo]::New([char]8) , [securestring]::New(), [System.DateTimeOffset]::Now.AddDays(-1).DateTime, [System.DateTimeOffset]::Now.AddYears(10).DateTime); } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([string]$subjectName, [string]$pfxFile, [securestring]$password) { return [X509CertHelper]::CreateSelfSignedCertificate($subjectName, [IO.FileInfo]::new($pfxFile), $password); } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([IO.FileInfo]$pfxFile, [securestring]$password, [bool]$throwOnFailure) { $Cert = $null; if ($pfxFile.Exists) { if ($null -ne $password) { [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxFile.FullName, $password) } else { [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($pfxFile.FullName) } } elseif ($throwOnFailure) { throw [System.IO.FileNotFoundException]::New($pfxFile.FullName) }; return $Cert } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([string]$subjectName, [IO.FileInfo]$pfxFile, [securestring]$password) { return [X509CertHelper]::CreateSelfSignedCertificate($subjectName, $pfxFile, $password, 2048, [datetime]::Now.AddDays(-1), [datetime]::Now.AddYears(10)); } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([string]$subjectName, [IO.FileInfo]$pfxFile, [securestring]$password, [int]$keySizeInBits, [datetime]$notBefore, [datetime]$notAfter) { [void][x509CertHelper]::IsValidDistinguishedName($subjectName, $true); $certificate = [X509CertHelper]::CreateSelfSignedCertificate($pfxFile, $password, $false); if ($null -ne $certificate) { return $certificate } $certificateRequest = [X509CertHelper]::GetCertificateRequest($subjectName, [Object]::new(), [System.Security.Cryptography.HashAlgorithmName]::SHA256, $keySizeInBits) return [X509CertHelper]::CreateSelfSignedCertificate($certificateRequest, $pfxFile , $password, $notBefore, $notAfter); } static [System.Security.Cryptography.X509Certificates.X509Certificate2] CreateSelfSignedCertificate([System.Security.Cryptography.X509Certificates.CertificateRequest]$certificateRequest, [IO.FileInfo]$pfxFile, [securestring]$password, [datetime]$notBefore, [datetime]$notAfter) { $certResult = [System.Security.Cryptography.X509Certificates.X509Certificate2]$certificateRequest.CreateSelfSigned($notBefore, $notAfter); $certRawData = if (![string]::IsNullOrWhiteSpace([Pscredential]::new(' ', $password).GetNetworkCredential().Password)) { $certResult.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $password) } else { $certResult.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx) } # Return it in PFX form to prevent windows throwing a security credentials not found error during sslStream.connectAsClient or HttpClient request. $Certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([byte[]]$certRawData); $certResult.Dispose(); if ($pfxFile -and $pfxFile.BaseName -ne ([string][char]8)) { [IO.File]::WriteAllBytes($pfxFile.FullName, $certRawData) } return $certificate } static [System.Security.Cryptography.X509Certificates.CertificateRequest] GetCertificateRequest([string]$subjectName) { return [X509CertHelper]::GetCertificateRequest($subjectName, [Object]::new(), [System.Security.Cryptography.HashAlgorithmName]::SHA256, 2048) } static [System.Security.Cryptography.X509Certificates.CertificateRequest] GetCertificateRequest([string]$subjectName, $key, [System.Security.Cryptography.HashAlgorithmName]$hashAlgorithm, [int]$keySizeInBits) { [void][x509CertHelper]::IsValidDistinguishedName($subjectName, $true); $certificateRequest = if ($key.GetType().Name -eq 'System.Security.Cryptography.ECDsa') { $ecdsa = [System.Security.Cryptography.ECDsa]::Create(); $ecdsa.GenerateKey([System.Security.Cryptography.ECCurve]::CreateFromFriendlyName("brainpoolP512r1")); $ecdsa.KeySize = $keySizeInBits [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($subjectName, $ecdsa, $hashAlgorithm); } else { [System.Security.Cryptography.X509Certificates.CertificateRequest]::new($subjectName, [System.Security.Cryptography.RSA]::Create($keySizeInBits), $hashAlgorithm, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1); } $certificateRequest.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true)); $certificateRequest.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($certificateRequest.PublicKey, $false)); $certificateRequest.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new([System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DataEncipherment, $true)); return $certificateRequest } static [string] GetThumbPrint([string]$certSubject, [string]$FriendlyName) { $CertStore = [System.Security.Cryptography.X509Certificates.X509Store]::new([System.Security.Cryptography.X509Certificates.StoreName]::My, [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser) $CertStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly); $Thumbprints = $CertStore.Certificates.Where({ $_.Subject -eq $certSubject -and $_.FriendlyName -eq $FriendlyName }).Thumbprint if ($Thumbprints.count -gt 1) { Write-Warning 'Ambiguous certs' }; $CertStore.Close(); return $Thumbprints[0]; } static [string] GetThumbprint([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { return $certificate.Thumbprint } static [void] SaveSelfSignedCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2]$X509Cert2) { # Stores X509Cert2 in certificate store. $CertStore = [System.Security.Cryptography.X509Certificates.X509Store]::new([System.Security.Cryptography.X509Certificates.StoreName]::My, [System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser); $CertStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite); $CertStore.Add($X509Cert2); $CertStore.Close() } static [byte[]] ExportCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { # Exports the certificate data in DER format. return $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert) } static [byte[]] ExportPfxCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { return $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx) } static [System.Security.Cryptography.X509Certificates.X509Certificate2] GetCertificate([byte[]]$certificateData) { return New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certificateData) } static [System.Security.Cryptography.X509Certificates.X509Certificate2] GetPfxCertificate([byte[]]$pfxData, [securestring]$password) { return New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxData, $password) } static [bool] TestCertificate([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { $passed_all_tests = $true # Check if the certificate has expired if ($certificate.NotAfter -lt [System.DateTime]::Now) { $passed_all_tests = $false Write-Host "Certificate has expired!" } # Check if the certificate has a specific key usage extension $keyUsageExtension = $certificate.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.15" } if ($keyUsageExtension) { $keyUsageFlags = $keyUsageExtension.Format($false) -replace ".*?\((.*)\).*", '$1' if ($keyUsageFlags -notlike "*DigitalSignature*") { $passed_all_tests = $false Write-Host "Certificate does not have DigitalSignature key usage!" } } else { $passed_all_tests = $false Write-Host "Certificate does not have KeyUsage extension!" } # Check if the certificate has a specific extended key usage $extendedKeyUsage = $certificate.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.37" } if ($extendedKeyUsage) { $extendedKeyUsageOids = $extendedKeyUsage.Format($false) -replace ".*?\((.*)\).*", '$1' if ($extendedKeyUsageOids -notlike "*1.3.6.1.5.5.7.3.1*") { $passed_all_tests = $false Write-Host "Certificate does not have Server Authentication extended key usage!" } } else { $passed_all_tests = $false Write-Host "Certificate does not have ExtendedKeyUsage extension!" } # More Custom Tests: $passed_all_tests = $passed_all_tests -and [X509CertHelper]::IsCertificateRevoked($certificate) -and [X509CertHelper]::IsKeyLengthValid($certificate) -and [X509CertHelper]::IsSignatureAlgorithmValid($certificate) -and [X509CertHelper]::ValidateCertificateChain($certificate) [X509CertHelper]::IsSANsValid($certificate, ('', '')) return $passed_all_tests } static [string] GetOUname([string]$X500DistinguishedName) { $ou = [string]::Empty $rx = [System.Text.RegularExpressions.Regex]::new("^(((CN=.*?))?)OU=(?<OUName>.*?(?=,))", "IgnoreCase") If ($rx.IsMatch($X500DistinguishedName) ) { $ou = $rx.Match($X500DistinguishedName).groups["OUName"].Value } return $ou } static [bool] IsValidDistinguishedName([string]$X500DistinguishedName) { return [X509CertHelper]::IsValidDistinguishedName($X500DistinguishedName, $false) } static [bool] IsValidDistinguishedName([string]$X500DistinguishedName, [bool]$throwOnFailure) { # .DESCRIPTION # A valid distinguished name string must follow a specific format, where each attribute is identified by a key and a value, separated by an equal sign =, and each attribute is separated by a comma , . For example, "CN=ddd" is a valid distinguished name string because it has a key CN and a value ddd separated by an equal sign =. $IsValid = ![string]::IsNullOrWhiteSpace($X500DistinguishedName) -and [regex]::IsMatch($X500DistinguishedName, '^(?:(?:\s*[a-zA-Z][a-zA-Z0-9-]*\s*=\s*[a-zA-Z0-9\s]*\s*,\s*)*(?:\s*[a-zA-Z][a-zA-Z0-9-]*\s*=\s*[a-zA-Z0-9\s]*\s*))?$') if (!$IsValid -and $throwOnFailure) { throw 'Please Provide a valid certificate subjectName' } return $IsValid } static [bool] IsCertificateRevoked([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { # .DESCRIPTION # Certificate Revocation Check: Perform a certificate revocation check by verifying if the certificate is listed in any certificate revocation lists (CRLs) # or if it has been revoked by the issuing certificate authority (CA). # Get the certificate chain: $chainPolicy = [System.Security.Cryptography.X509Certificates.X509ChainPolicy]::new(); $chainPolicy.RevocationFlag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EntireChain $chainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online $certificateChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new(); $certificateChain.ChainPolicy = $chainPolicy $certificateChain.Build($certificate) # Check if any certificate in the chain is revoked foreach ($element in $certificateChain.ChainElements) { foreach ($status in $element.ChainElementStatus) { if ($status.Status -eq [System.Security.Cryptography.X509Certificates.X509ChainStatusFlags]::Revoked) { Write-Host "Certificate is revoked" -ForegroundColor Green return $true } } } Write-Host "Certificate is not revoked" -ForegroundColor Red return $false } static [bool] IsKeyLengthValid([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [int]$minKeyLength) { # .DESCRIPTION # Key Length Check: Check the length of the public key in the certificate and ensure it meets your desired security requirements. # For example, you can check if the key length is at least 2048 bits for RSA certificates. $publicKey = $certificate.GetPublicKey(); $publicKeyLength = $publicKey.Length * 8 # Convert byte length to bit length if ($publicKeyLength -ge $minKeyLength) { Write-Host "Key length is valid" -ForegroundColor Green return $true } else { Write-Host "Key length is not valid" -ForegroundColor Red return $false } } static [bool] IsSignatureAlgorithmValid([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [string]$requiredAlgorithm) { # .DESCRIPTION # Validate the signature algorithm used to sign the certificate. # Ensure it meets your desired security standards. Ex: you can check if the certificate is signed using a strong algorithm like SHA-256. # $requiredAlgorithm can be "SHA256", "SHA384", or "SHA512", among others. if ($certificate.SignatureAlgorithm.FriendlyName -eq $requiredAlgorithm) { Write-Host "Signature algorithm is valid" -ForegroundColor Green return $true } else { Write-Host "Signature algorithm is not valid" -ForegroundColor Red return $false } } static [bool] ValidateCertificateChain([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate) { # .DESCRIPTION # Validate the entire certificate chain up to the trusted root certificate. # Ensure that all intermediate certificates are present and correctly ordered in the chain, and that each certificate in the chain is valid and not expired. $chainPolicy = [System.Security.Cryptography.X509Certificates.X509ChainPolicy]::new(); $chainPolicy.RevocationFlag = [System.Security.Cryptography.X509Certificates.X509RevocationFlag]::EntireChain $chainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::Online $chainPolicy.VerificationFlags = [System.Security.Cryptography.X509Certificates.X509VerificationFlags]::NoFlag $certificateChain = [System.Security.Cryptography.X509Certificates.X509Chain]::new(); $certificateChain.ChainPolicy = $chainPolicy $certificateChain.ChainPolicy.ExtraStore.Add($certificate) $certificateChain.Build($certificate) if ($certificateChain.ChainStatus.Length -eq 0) { Write-Host "Certificate chain is valid" -ForegroundColor Green return $true } else { Write-Host "Certificate chain is not valid" -ForegroundColor Red return $false } } static [bool] IsSANsValid([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [string[]]$requiredSANs) { # Check if the certificate includes the required Subject Alternative Names (SANs) for your specific use case, such as DNS names, IP addresses, or email addresses. # Ex: # $requiredSANs = @( # "www.example.com", # "subdomain.example.com", # "192.168.0.1" # ) $certificateSANs = $certificate.Extensions.Where({ $_.Oid.FriendlyName -eq "Subject Alternative Name" }).Foreach({ $_.Format($false) -split ', ' }); foreach ($requiredSAN in $requiredSANs) { if ($certificateSANs -contains $requiredSAN) { return $true # Required SAN found in the certificate } } return $false # Required SAN not found in the certificate } static [bool] IsCertificatePolicyValid([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [string]$requiredPolicy) { # Validate if the certificate adheres to specific certificate policies defined by your organization or industry standards. # /!\ Not sure how to write this one! $certificatePolicies = $certificate.Extensions | Where-Object { $_.Oid.FriendlyName -eq "Certificate Policies" } | ForEach-Object { $_.Format($false) -split ', ' } if ($certificatePolicies -contains $requiredPolicy) { return $true # Required certificate policy is found } else { return $false # Required certificate policy is not found } } static [bool] IsExtendedValidationValid([System.Security.Cryptography.X509Certificates.X509Certificate2]$certificate, [System.Security.Cryptography.Oid]$ValidationOID) { # If you are dealing with Extended Validation (EV) certificates, perform additional checks specific to EV requirements, # such as verifying the presence of the EV OID in the certificate. $extendedValidationOID = $ValidationOID.Value $certificateExtensions = $certificate.Extensions foreach ($extension in $certificateExtensions) { if ($extension.Oid.Value -eq $extendedValidationOID) { Write-Host "Extended Validation flag is present" -ForegroundColor Green return $true } } Write-Host "Extended Validation flag is not present" -ForegroundColor Red return $false } static [IO.FileInfo] GetOpenssl () { # Return the path to openssl executable file & Will install it if not found :) $file = [IO.FileInfo](Get-Command -Name OpenSSL -Type Application -ErrorAction Ignore).Source if (!$file -or !$file.Exists) { if (!(Get-Command -Name Install-OpenSSL -Type ExternalScript -ErrorAction Ignore)) { Install-Script -Name Install-OpenSSL -Repository PSGallery -Scope CurrentUser } Install-OpenSSL } return $file } } |