build/Sign-Module.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$ModulePath,

    [Parameter(Mandatory)]
    [string]$PfxBase64,

    [Parameter(Mandatory)]
    [string]$PfxPassword,

    [Parameter()]
    [uri]$TimestampServer = 'https://timestamp.digicert.com'
)

$ErrorActionPreference = 'Stop'
$moduleRoot = (Resolve-Path -Path $ModulePath).Path
$pfxCertificates = [Security.Cryptography.X509Certificates.X509Certificate2Collection]::new()
$certificate = $null
$pfxBytes = $null

try {
    Write-Host "[$(Get-Date -Format o)] Decoding the code-signing PFX."
    $pfxBytes = [Convert]::FromBase64String($PfxBase64)

    Write-Host "[$(Get-Date -Format o)] Loading the code-signing certificate in memory."
    $pfxCertificates.Import(
        $pfxBytes,
        $PfxPassword,
        [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
    )

    $codeSigningOid = '1.3.6.1.5.5.7.3.3'
    $codeSigningCertificates = @(
        foreach ($candidate in $pfxCertificates) {
            $ekuExtension = $candidate.Extensions |
                Where-Object {
                    $_ -is [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]
                } |
                Select-Object -First 1
            $ekuOids = @(
                if ($null -ne $ekuExtension) {
                    $ekuExtension.EnhancedKeyUsages | ForEach-Object Value
                }
            )

            Write-Host (
                "Loaded certificate: Subject='{0}', Thumbprint='{1}', HasPrivateKey={2}, EKUs='{3}'" -f
                $candidate.Subject,
                $candidate.Thumbprint,
                $candidate.HasPrivateKey,
                ($ekuOids -join ',')
            )

            if ($candidate.HasPrivateKey -and $ekuOids -contains $codeSigningOid) {
                $candidate
            }
        }
    )
    $certificate = $codeSigningCertificates | Select-Object -First 1

    if ($null -eq $certificate) {
        throw 'The PFX does not contain a certificate with a private key that is valid for code signing.'
    }

    $isSelfSigned = $certificate.Subject -eq $certificate.Issuer
    if ($isSelfSigned) {
        Write-Warning 'Using a self-signed certificate. Signature trust cannot be established on the runner.'
    }

    $now = Get-Date
    if ($now -lt $certificate.NotBefore -or $now -gt $certificate.NotAfter) {
        throw 'The code-signing certificate is not currently valid.'
    }

    $moduleFiles = @(
        Get-Item -Path (
            Join-Path $moduleRoot 'M365.Toolkit.psd1'
        ), (
            Join-Path $moduleRoot 'M365.Toolkit.psm1'
        )
        Get-ChildItem -Path (
            Join-Path $moduleRoot 'src\Private'
        ), (
            Join-Path $moduleRoot 'src\Public'
        ) -Recurse -File |
            Where-Object Extension -In '.ps1', '.psm1', '.psd1', '.ps1xml', '.cdxml'
    )

    foreach ($file in $moduleFiles) {
        Write-Host "[$(Get-Date -Format o)] Signing $($file.FullName)"
        $signingParameters = @{
            FilePath      = $file.FullName
            Certificate   = $certificate
            HashAlgorithm = 'SHA256'
            IncludeChain  = 'All'
        }
        if (-not $isSelfSigned) {
            $signingParameters.TimestampServer = $TimestampServer.AbsoluteUri
        }

        $signature = Set-AuthenticodeSignature @signingParameters
        Write-Host "[$(Get-Date -Format o)] Signature status: $($signature.Status)"

        $acceptableStatus = if ($isSelfSigned) {
            $signature.Status -in @(
                [System.Management.Automation.SignatureStatus]::Valid,
                [System.Management.Automation.SignatureStatus]::UnknownError
            )
        }
        else {
            $signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid
        }

        if (
            -not $acceptableStatus -or
            $null -eq $signature.SignerCertificate -or
            $signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint
        ) {
            throw "Failed to sign '$($file.FullName)': $($signature.StatusMessage)"
        }
    }

    $invalidSignatures = @(
        foreach ($file in $moduleFiles) {
            $signature = Get-AuthenticodeSignature -FilePath $file.FullName
            $acceptableStatus = if ($isSelfSigned) {
                $signature.Status -in @(
                    [System.Management.Automation.SignatureStatus]::Valid,
                    [System.Management.Automation.SignatureStatus]::UnknownError
                )
            }
            else {
                $signature.Status -eq [System.Management.Automation.SignatureStatus]::Valid
            }

            if (
                -not $acceptableStatus -or
                $null -eq $signature.SignerCertificate -or
                $signature.SignerCertificate.Thumbprint -ne $certificate.Thumbprint
            ) {
                $signature
            }
        }
    )
    if ($invalidSignatures.Count -gt 0) {
        throw "Signature validation failed for: $($invalidSignatures.Path -join ', ')"
    }

    Write-Host "Signed and validated $($moduleFiles.Count) module files."
}
finally {
    foreach ($pfxCertificate in $pfxCertificates) {
        $pfxCertificate.Dispose()
    }
    if ($null -ne $pfxBytes) {
        [Array]::Clear($pfxBytes, 0, $pfxBytes.Length)
    }
}