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
  }
}