Get-MsIdCBACertificateUserIdFromCertificate.ps1

# ------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
# ------------------------------------------------------------------------------

<#
.SYNOPSIS
    Generates an object representing all the values contained in a certificate file that can be used in Entra ID for configuring CertificateUserIDs in Certificate-Based Authentication.

.DESCRIPTION
    Retrieves and returns an object with the properties 'PrincipalName', 'RFC822Name', 'IssuerAndSubject', 'Subject', 'SKI', 'SHA1PublicKey', and 'IssuerAndSerialNumber' from a certificate file for use in CertificateUserIDs configuration in Certificate-Based Authentication, according to the guidelines outlined in the Microsoft documentation for certificate-based authentication

.PARAMETER Path
    The path to the certificate file. The file can be in .cer or .pem format.

.PARAMETER Certificate
    An X509Certificate2 object

.PARAMETER CertificateMapping
    The certificate mapping property to retrieve. Valid values are PrincipalName, RFC822Name, IssuerAndSubject, Subject, SKI, SHA1PublicKey, and IssuerAndSerialNumber.

.EXAMPLE
    PS > Get-MsIdCBACertificateUserIdFromCertificate -Path "C:\path\to\certificate.cer"

    This command retrieves all the possible certificate mappings and returns an object to represent them.

.EXAMPLE
    PS > Get-MsIdCBACertificateUserIdFromCertificate -Certificate $cert

    This command retrieves all the possible certificate mappings and returns an object to represent them.

.EXAMPLE
    PS > Get-MsIdCBACertificateUserIdFromCertificate -Path "C:\path\to\certificate.cer" -CertificateMapping Subject

    This command retrieves and returns the PrincipalName property.

.OUTPUTS
    Returns an object containing the certificateUserIDs that can be used with the givin certificate.

    @{
        PrincipalName = "X509:<PN>bob@woodgrove.com"
        RFC822Name = "X509:<RFC822>user@woodgrove.com"
        IssuerAndSubject = "X509:<I>DC=com,DC=contoso,CN=CONTOSO-DC-CA<S>DC=com,DC=contoso,OU=UserAccounts,CN=mfatest"
        Subject = "X509:<S>DC=com,DC=contoso,OU=UserAccounts,CN=mfatest"
        SKI = "X509:<SKI>aB1cD2eF3gH4iJ5kL6mN7oP8qR"
        SHA1PublicKey = "X509:<SHA1-PUKEY>cD2eF3gH4iJ5kL6mN7oP8qR9sT"
        IssuerAndSerialNumber = "X509:<I>DC=com,DC=contoso,CN=CONTOSO-DC-CA<SR>eF3gH4iJ5kL6mN7oP8qR9sT0uV"
    }

#>


function Get-MsIdCBACertificateUserIdFromCertificate {
    param (
        [Parameter(Mandatory = $false)]
        [string]$Path,
        [Parameter(Mandatory = $false)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
        [Parameter(Mandatory = $false)]
        [ValidateSet("PrincipalName", "RFC822Name", "IssuerAndSubject", "Subject", "SKI", "SHA1PublicKey", "IssuerAndSerialNumber")]
        [string]$CertificateMapping
    )

    function Get-Certificate {
        param (
            [string]$filePath
        )
        if ($filePath.EndsWith(".cer")) {
            return [System.Security.Cryptography.X509Certificates.X509Certificate]::new($filePath)
        } elseif ($filePath.EndsWith(".pem")) {
            $pemContent = Get-Content -Path $filePath -Raw
            $pemContent = $pemContent -replace "-----BEGIN CERTIFICATE-----", ""
            $pemContent = $pemContent -replace "-----END CERTIFICATE-----", ""
            $pemContent = $pemContent -replace  "(\r\n|\n|\r)", ""
            $pemBytes = [Convert]::FromBase64String($pemContent)
            $certificate = [System.Security.Cryptography.X509Certificates.X509Certificate]::new($pemBytes)

            return $certificate
        } else {
            throw "Unsupported certificate format. Please provide a .cer or .pem file."
        }
    }

    function Get-DistinguishedNameAsString {
        param (
            [System.Security.Cryptography.X509Certificates.X500DistinguishedName]$distinguishedName
        )

        $dn = $distinguishedName.Decode([System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::UseNewLines -bor [System.Security.Cryptography.X509Certificates.X500DistinguishedNameFlags]::DoNotUsePlusSign)
        
        $dn = $dn -replace "(\r\n|\n|\r)", ","
        return $dn.TrimEnd(',')
    }

    function Get-SerialNumberAsLittleEndianHexString {
        param (
            [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
        )

        $littleEndianSerialNumber = $cert.GetSerialNumber()

        if ($littleEndianSerialNumber.Length -eq 0)
        {
            return ""
        }

        [System.Array]::Reverse($littleEndianSerialNumber)
        $hexString = -join ($littleEndianSerialNumber | ForEach-Object { $_.ToString("x2") })
        return $hexString
    }

    function Get-SubjectKeyIdentifier {
        param (
            [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
        )
        foreach ($extension in $cert.Extensions) {
            if ($extension.Oid.Value -eq "2.5.29.14") {
                $ski = New-Object System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension -ArgumentList $extension, $false
                return $ski.SubjectKeyIdentifier
            }
        }

        return ""
    }

    # Function to generate certificate mapping fields
    function Get-CertificateMappingFields {
        param (
            [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
        )
        $subject = Get-DistinguishedNameAsString -distinguishedName $cert.SubjectName
        $issuer = Get-DistinguishedNameAsString -distinguishedName $cert.IssuerName
        $serialNumber = Get-SerialNumberAsLittleEndianHexString -cert $cert
        $thumbprint = $cert.Thumbprint
        $principalName = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::UpnName, $false)
        $emailName = $cert.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::EmailName, $false)
        $subjectKeyIdentifier = Get-SubjectKeyIdentifier -cert $cert
        $sha1PublicKey = $cert.GetCertHashString()

        return @{
            "SubjectName" = $subject
            "IssuerName" = $issuer
            "SerialNumber" = $serialNumber
            "Thumbprint" = $thumbprint
            "PrincipalName" = $principalName
            "EmailName" = $emailName
            "SubjectKeyIdentifier" = $subjectKeyIdentifier
            "Sha1PublicKey" = $sha1PublicKey
        }
    }

    function Get-CertificateUserIds {
        param (
            [System.Security.Cryptography.X509Certificates.X509Certificate2]$cert
        )

        $mappingFields = Get-CertificateMappingFields -cert $cert

        $certUserIDs = @{
            "PrincipalName" = ""
            "RFC822Name" = ""
            "IssuerAndSubject" = "" 
            "Subject" = ""
            "SKI" = ""
            "SHA1PublicKey" = "" 
            "IssuerAndSerialNumber" = ""
        }

        if (-not [string]::IsNullOrWhiteSpace($mappingFields.PrincipalName)) 
        {
            $certUserIDs.PrincipalName = "X509:<PN>$($mappingFields.PrincipalName)"
        }

        if (-not [string]::IsNullOrWhiteSpace($mappingFields.EmailName)) 
        {
            $certUserIDs.RFC822Name = "X509:<RFC822>$($mappingFields.EmailName)"
        }

        if ((-not [string]::IsNullOrWhiteSpace($mappingFields.IssuerName)) -and (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectName))) 
        {
            $certUserIDs.IssuerAndSubject = "X509:<I>$($mappingFields.IssuerName)<S>$($mappingFields.SubjectName)"
        }

        if (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectName)) 
        {
            $certUserIDs.Subject = "X509:<S>$($mappingFields.SubjectName)"
        }

        if (-not [string]::IsNullOrWhiteSpace($mappingFields.SubjectKeyIdentifier)) 
        {
            $certUserIDs.SKI = "X509:<SKI>$($mappingFields.SubjectKeyIdentifier)"
        }

        if (-not [string]::IsNullOrWhiteSpace($mappingFields.Sha1PublicKey)) 
        {
            $certUserIDs.SHA1PublicKey = "X509:<SHA1-PUKEY>$($mappingFields.Sha1PublicKey)"
        }

        if ((-not [string]::IsNullOrWhiteSpace($mappingFields.IssuerName)) -and (-not [string]::IsNullOrWhiteSpace($mappingFields.SerialNumber))) 
        {
            $certUserIDs.IssuerAndSerialNumber = "X509:<I>$($mappingFields.IssuerName)<SR>$($mappingFields.SerialNumber)"
        }

        return $certUserIDs
    }

    function Main
    {
        $cert = $Certificate
        if ($null -eq $cert)
        {
            $cert = Get-Certificate -filePath $Path
        }

        $mappings = Get-CertificateUserIds -cert $cert
        
        if ($CertificateMapping -eq "")
        {
            return $mappings
        }
        else
        {
            $value = $mappings[$CertificateMapping]
            return "$($value)"
        }
    }

    # Call main function
    return Main
}