Support/Package/Schema/Eigenverft.Manifested.Package.Package.Trust.ps1

<#
    Eigenverft.Manifested.Package.Package.Trust
    Catalog-signature canonicalization, certificate, and PackageTrustInventory.json helpers.
#>


$script:PackageDefinitionSignatureFormat = 'embedded-json-rsa-sha256-v1'
$script:PackageDefinitionSignedContentKind = 'canonicalDefinitionExcludingSignatureValue'

function ConvertFrom-PackageSecureString {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]$SecureString
    )

    $ptr = [IntPtr]::Zero
    try {
        $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
        return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
    }
    finally {
        if ($ptr -ne [IntPtr]::Zero) {
            [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
        }
    }
}

function ConvertTo-PackageCanonicalJson {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $Value) {
        return 'null'
    }
    if ($Value -is [bool]) {
        if ($Value) {
            return 'true'
        }
        return 'false'
    }
    if ($Value -is [string] -and $Value -match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$') {
        try {
            $dateOffset = [DateTimeOffset]::Parse([string]$Value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal)
            return ConvertTo-PackageJsonEscapedString -Value ($dateOffset.UtcDateTime.ToString('o', [Globalization.CultureInfo]::InvariantCulture))
        }
        catch {
        }
    }
    if ($Value -is [string] -or $Value -is [char] -or $Value -is [guid]) {
        return ConvertTo-PackageJsonEscapedString -Value ([string]$Value)
    }
    if ($Value -is [datetime]) {
        $dateText = ([datetime]$Value).ToUniversalTime().ToString('o', [Globalization.CultureInfo]::InvariantCulture)
        return ConvertTo-PackageJsonEscapedString -Value $dateText
    }
    if ($Value -is [byte] -or $Value -is [sbyte] -or
        $Value -is [int16] -or $Value -is [uint16] -or
        $Value -is [int] -or $Value -is [uint32] -or
        $Value -is [long] -or $Value -is [uint64] -or
        $Value -is [decimal]) {
        return ([System.Convert]::ToString($Value, [Globalization.CultureInfo]::InvariantCulture))
    }
    if ($Value -is [single] -or $Value -is [double]) {
        $doubleValue = [double]$Value
        if ([double]::IsNaN($doubleValue) -or [double]::IsInfinity($doubleValue)) {
            throw 'Canonical JSON cannot represent NaN or Infinity.'
        }
        return $doubleValue.ToString('R', [Globalization.CultureInfo]::InvariantCulture)
    }
    if ($Value -is [System.Collections.IDictionary]) {
        $properties = @(
            foreach ($key in @($Value.Keys)) {
                [pscustomobject]@{
                    Name  = [string]$key
                    Value = $Value[$key]
                }
            }
        ) | Sort-Object -Property Name

        $parts = @(
            foreach ($property in @($properties)) {
                '{0}:{1}' -f (ConvertTo-PackageJsonEscapedString -Value $property.Name), (ConvertTo-PackageCanonicalJson -Value $property.Value)
            }
        )
        return '{' + ($parts -join ',') + '}'
    }
    if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string])) {
        $items = @(
            foreach ($item in $Value) {
                ConvertTo-PackageCanonicalJson -Value $item
            }
        )
        return '[' + ($items -join ',') + ']'
    }

    $objectProperties = @(
        foreach ($property in @($Value.PSObject.Properties)) {
            if ($property.MemberType -notin @('NoteProperty', 'Property', 'AliasProperty')) {
                continue
            }
            [pscustomobject]@{
                Name  = [string]$property.Name
                Value = $property.Value
            }
        }
    ) | Sort-Object -Property Name

    $objectParts = @(
        foreach ($property in @($objectProperties)) {
            '{0}:{1}' -f (ConvertTo-PackageJsonEscapedString -Value $property.Name), (ConvertTo-PackageCanonicalJson -Value $property.Value)
        }
    )
    return '{' + ($objectParts -join ',') + '}'
}

function ConvertTo-PackageUtf8Bytes {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Text
    )

    $encoding = [System.Text.UTF8Encoding]::new($false)
    return $encoding.GetBytes($Text)
}

function Get-PackageBytesSha256Text {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]]$Bytes
    )

    $sha256 = [System.Security.Cryptography.SHA256]::Create()
    try {
        return (($sha256.ComputeHash($Bytes) | ForEach-Object { $_.ToString('x2') }) -join '')
    }
    finally {
        $sha256.Dispose()
    }
}

function Copy-PackageObjectViaJson {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object]$InputObject
    )

    if ($null -eq $InputObject) {
        return $null
    }

    return (($InputObject | ConvertTo-Json -Depth 80) | ConvertFrom-Json)
}

function Remove-PackageDefinitionSignatureValueFromObject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition
    )

    if ($Definition.PSObject.Properties['definitionPublication'] -and
        $Definition.definitionPublication -and
        $Definition.definitionPublication.PSObject.Properties['definitionSignature'] -and
        $Definition.definitionPublication.definitionSignature -and
        $Definition.definitionPublication.definitionSignature.PSObject.Properties['signatureValue']) {
        $Definition.definitionPublication.definitionSignature.PSObject.Properties.Remove('signatureValue')
    }
}

function Get-PackageDefinitionSignableContent {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition
    )

    $clone = Copy-PackageObjectViaJson -InputObject $Definition
    Remove-PackageDefinitionSignatureValueFromObject -Definition $clone
    $canonicalJson = ConvertTo-PackageCanonicalJson -Value $clone
    $bytes = ConvertTo-PackageUtf8Bytes -Text $canonicalJson

    return [pscustomobject]@{
        CanonicalJson = $canonicalJson
        Bytes         = $bytes
        Sha256        = Get-PackageBytesSha256Text -Bytes $bytes
    }
}

function Set-PackageObjectProperty {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$InputObject,

        [Parameter(Mandatory = $true)]
        [string]$Name,

        [AllowNull()]
        [object]$Value
    )

    if ($InputObject.PSObject.Properties[$Name]) {
        $InputObject.PSObject.Properties[$Name].Value = $Value
        return
    }

    $InputObject | Add-Member -MemberType NoteProperty -Name $Name -Value $Value
}

function ConvertTo-PackageSafeFileName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $trimmed = $Value.Trim()
    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        return 'PackageSigning'
    }

    $safe = ($trimmed -replace '[\\/:*?"<>|]+', '-' -replace '\s+', '-').Trim('-')
    if ([string]::IsNullOrWhiteSpace($safe)) {
        return 'PackageSigning'
    }

    return $safe
}

function ConvertTo-PackageX500NameValue {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Value
    )

    $trimmed = $Value.Trim()
    if ([string]::IsNullOrWhiteSpace($trimmed)) {
        throw 'Certificate subject values must not be empty.'
    }

    return ($trimmed -replace '\\', '\\' -replace '([,+"<>;=])', '\$1')
}

function New-PackageCertificateSubject {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CommonName,

        [AllowNull()]
        [string]$Organization = $null,

        [AllowNull()]
        [string]$OrganizationalUnit = $null,

        [AllowNull()]
        [string]$Country = $null
    )

    if ([string]::IsNullOrWhiteSpace($CommonName)) {
        throw 'CommonName must not be empty.'
    }
    if (-not [string]::IsNullOrWhiteSpace($Country) -and $Country.Trim() -notmatch '^[A-Za-z]{2}$') {
        throw "Country '$Country' is invalid. Use a two-letter ISO country code such as 'DE' or 'US'."
    }

    $parts = New-Object System.Collections.Generic.List[string]
    $parts.Add(('CN={0}' -f (ConvertTo-PackageX500NameValue -Value $CommonName))) | Out-Null
    if (-not [string]::IsNullOrWhiteSpace($Organization)) {
        $parts.Add(('O={0}' -f (ConvertTo-PackageX500NameValue -Value $Organization))) | Out-Null
    }
    if (-not [string]::IsNullOrWhiteSpace($OrganizationalUnit)) {
        $parts.Add(('OU={0}' -f (ConvertTo-PackageX500NameValue -Value $OrganizationalUnit))) | Out-Null
    }
    if (-not [string]::IsNullOrWhiteSpace($Country)) {
        $parts.Add(('C={0}' -f $Country.Trim().ToUpperInvariant())) | Out-Null
    }

    return ($parts.ToArray() -join ', ')
}

function Get-PackageDefaultSigningDirectory {
    [CmdletBinding()]
    param()

    $documentsPath = [Environment]::GetFolderPath([Environment+SpecialFolder]::MyDocuments)
    if ([string]::IsNullOrWhiteSpace($documentsPath)) {
        $documentsPath = Join-Path $HOME 'Documents'
    }

    return [System.IO.Path]::GetFullPath((Join-Path $documentsPath 'Eigenverft.Package\Signing'))
}

function Get-PackageSigningPasswordEnvironmentVariableName {
    [CmdletBinding()]
    param()

    return 'EVF_PACKAGE_SIGNING_PASSWORD'
}

function Get-PackageSigningPasswordDescriptorPath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$PfxPath
    )

    return [System.IO.Path]::GetFullPath([System.IO.Path]::ChangeExtension($PfxPath, '.json'))
}

function New-PackageSigningPasswordDescriptorDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]$Password
    )

    $now = [DateTime]::UtcNow.ToString('o')
    return [pscustomobject][ordered]@{
        schemaVersion         = 1
        kind                  = 'catalogSigningPassword'
        protectedPasswordKind = 'dpapi-current-user-securestring'
        protectedPassword     = Protect-PackageSigningProfilePassword -Password $Password
        createdAtUtc          = $now
        updatedAtUtc          = $now
    }
}

function Assert-PackageSigningPasswordDescriptorSchema {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DescriptorInfo
    )

    $document = $DescriptorInfo.Document
    foreach ($requiredProperty in @('schemaVersion', 'kind', 'protectedPasswordKind', 'protectedPassword')) {
        if (-not $document.PSObject.Properties[$requiredProperty] -or [string]::IsNullOrWhiteSpace([string]$document.$requiredProperty)) {
            throw "Package signing password descriptor '$($DescriptorInfo.Path)' is missing '$requiredProperty'."
        }
    }
    if (-not [string]::Equals([string]$document.kind, 'catalogSigningPassword', [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Package signing password descriptor '$($DescriptorInfo.Path)' has unsupported kind '$($document.kind)'."
    }
    if (-not [string]::Equals([string]$document.protectedPasswordKind, 'dpapi-current-user-securestring', [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Package signing password descriptor '$($DescriptorInfo.Path)' has unsupported protectedPasswordKind '$($document.protectedPasswordKind)'."
    }
    foreach ($forbiddenProperty in @('pfxPath', 'certificatePath', 'trustExportPath')) {
        if ($document.PSObject.Properties[$forbiddenProperty]) {
            throw "Package signing password descriptor '$($DescriptorInfo.Path)' must not contain '$forbiddenProperty'. The PFX is resolved from the adjacent file name."
        }
    }
}

function Save-PackageSigningPasswordDescriptor {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [securestring]$Password
    )

    Save-PackageJsonDocument -Path $Path -Document (New-PackageSigningPasswordDescriptorDocument -Password $Password)
}

function Get-PackageSigningPasswordFromDescriptor {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $descriptorInfo = Read-PackageJsonDocument -Path $Path
    Assert-PackageSigningPasswordDescriptorSchema -DescriptorInfo $descriptorInfo
    return Unprotect-PackageSigningProfilePassword -ProtectedPassword ([string]$descriptorInfo.Document.protectedPassword)
}

function Get-PackageSigningPasswordFromEnvironment {
    [CmdletBinding()]
    param()

    $variableName = Get-PackageSigningPasswordEnvironmentVariableName
    $passwordText = [Environment]::GetEnvironmentVariable($variableName, 'Process')
    if ([string]::IsNullOrEmpty($passwordText)) {
        return $null
    }

    return ConvertTo-SecureString -String $passwordText -AsPlainText -Force
}

function ConvertTo-PackageSigningSelectorKey {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Value
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        return ''
    }

    return (([string]$Value).ToLowerInvariant() -replace '[^a-z0-9]', '')
}

function Get-PackageSigningPfxPathByName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name
    )

    $signingDirectory = Get-PackageDefaultSigningDirectory
    if (-not (Test-Path -LiteralPath $signingDirectory -PathType Container)) {
        throw "Package signing directory '$signingDirectory' does not exist. Create a signing certificate with New-PackageSigningCertificate -Name '$Name' first."
    }

    $selectorKey = ConvertTo-PackageSigningSelectorKey -Value $Name
    $matchingPfxFiles = @(
        Get-ChildItem -LiteralPath $signingDirectory -Filter '*.catalog-signing.pfx' -File -Recurse | Where-Object {
            $baseName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name)
            $friendlyName = ($baseName -replace '\.catalog-signing$', '')
            $folderName = Split-Path -Leaf $_.DirectoryName
            $selectorKey -in @(
                ConvertTo-PackageSigningSelectorKey -Value $baseName
                ConvertTo-PackageSigningSelectorKey -Value $friendlyName
                ConvertTo-PackageSigningSelectorKey -Value $folderName
            )
        }
    )

    if ($matchingPfxFiles.Count -eq 0) {
        throw "No package signing certificate named '$Name' was found under '$signingDirectory'. Use -Cert with a PFX path or create one with New-PackageSigningCertificate -Name '$Name'."
    }
    if ($matchingPfxFiles.Count -gt 1) {
        throw "Package signing certificate name '$Name' is ambiguous under '$signingDirectory'. Use -Cert with the full PFX path."
    }

    return [System.IO.Path]::GetFullPath($matchingPfxFiles[0].FullName)
}

function Resolve-PackageSigningCertificateReference {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [string]$Cert = $null,

        [AllowNull()]
        [securestring]$Password = $null
    )

    if ([string]::IsNullOrWhiteSpace($Cert)) {
        throw "Sign-PackageDefinition requires -Cert. Use a friendly name, a .pfx path, or an adjacent .catalog-signing.json descriptor."
    }

    $reference = $Cert.Trim()
    $resolvedReference = $null
    if (Test-Path -LiteralPath $reference -PathType Leaf) {
        $resolvedReference = (Resolve-Path -LiteralPath $reference -ErrorAction Stop).Path
    }
    elseif ([System.IO.Path]::IsPathRooted($reference) -or $reference -match '[\\/]') {
        throw "Package signing certificate reference '$reference' does not exist."
    }
    else {
        $resolvedReference = Get-PackageSigningPfxPathByName -Name $reference
    }

    $extension = [System.IO.Path]::GetExtension($resolvedReference)
    $pfxPath = $null
    $descriptorPath = $null
    if ([string]::Equals($extension, '.json', [System.StringComparison]::OrdinalIgnoreCase)) {
        $descriptorPath = [System.IO.Path]::GetFullPath($resolvedReference)
        $pfxPath = [System.IO.Path]::GetFullPath([System.IO.Path]::ChangeExtension($descriptorPath, '.pfx'))
        if (-not (Test-Path -LiteralPath $pfxPath -PathType Leaf)) {
            throw "Package signing descriptor '$descriptorPath' requires adjacent PFX '$pfxPath'."
        }
    }
    elseif ([string]::Equals($extension, '.pfx', [System.StringComparison]::OrdinalIgnoreCase) -or
        [string]::Equals($extension, '.p12', [System.StringComparison]::OrdinalIgnoreCase)) {
        $pfxPath = [System.IO.Path]::GetFullPath($resolvedReference)
        $candidateDescriptorPath = Get-PackageSigningPasswordDescriptorPath -PfxPath $pfxPath
        if (Test-Path -LiteralPath $candidateDescriptorPath -PathType Leaf) {
            $descriptorPath = $candidateDescriptorPath
        }
    }
    elseif ($extension -in @('.cer', '.crt', '.pem')) {
        throw "Package signing certificate reference '$resolvedReference' is public-only. Use the private .pfx or adjacent .catalog-signing.json descriptor for signing."
    }
    else {
        throw "Package signing certificate reference '$resolvedReference' must be a .pfx, .p12, or .json file."
    }

    $passwordSource = 'parameter'
    if ($null -eq $Password) {
        if (-not [string]::IsNullOrWhiteSpace($descriptorPath)) {
            $Password = Get-PackageSigningPasswordFromDescriptor -Path $descriptorPath
            $passwordSource = 'descriptor'
        }
        else {
            $Password = Get-PackageSigningPasswordFromEnvironment
            if ($Password) {
                $passwordSource = 'environment'
            }
        }
    }

    if ($null -eq $Password) {
        $environmentVariableName = Get-PackageSigningPasswordEnvironmentVariableName
        throw "Password is required for signing certificate '$pfxPath'. Use -Password, create adjacent descriptor '$(Get-PackageSigningPasswordDescriptorPath -PfxPath $pfxPath)', or set environment variable '$environmentVariableName'."
    }

    return [pscustomobject]@{
        PfxPath        = $pfxPath
        DescriptorPath = $descriptorPath
        Password       = $Password
        PasswordSource = $passwordSource
    }
}

function Protect-PackageSigningProfilePassword {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [securestring]$Password
    )

    return ConvertFrom-SecureString -SecureString $Password
}

function Unprotect-PackageSigningProfilePassword {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ProtectedPassword
    )

    return ConvertTo-SecureString -String $ProtectedPassword -ErrorAction Stop
}

function Get-PackageSigningProfileSummaries {
    [CmdletBinding()]
    param()

    $signingDirectory = Get-PackageDefaultSigningDirectory
    if (-not (Test-Path -LiteralPath $signingDirectory -PathType Container)) {
        return
    }

    foreach ($pfxFile in @(Get-ChildItem -LiteralPath $signingDirectory -Filter '*.catalog-signing.pfx' -File -Recurse)) {
        $pfxPath = [System.IO.Path]::GetFullPath($pfxFile.FullName)
        $descriptorPath = Get-PackageSigningPasswordDescriptorPath -PfxPath $pfxPath
        $certificatePath = [System.IO.Path]::ChangeExtension($pfxPath, '.cer')
        if (-not (Test-Path -LiteralPath $certificatePath -PathType Leaf)) {
            $certificatePath = [System.IO.Path]::ChangeExtension($pfxPath, '.pem')
        }

        $baseName = [System.IO.Path]::GetFileNameWithoutExtension($pfxFile.Name)
        $name = ($baseName -replace '\.catalog-signing$', '')
        $publisherId = $name
        $publisherName = $name
        $keyThumbprint = $null
        $certificateSubject = $null
        if (Test-Path -LiteralPath $certificatePath -PathType Leaf) {
            $certificate = Import-PackageCertificate -Path $certificatePath
            try {
                $resolvedPublisherId = Resolve-PackagePublisherIdFromCertificate -Certificate $certificate
                if (-not [string]::IsNullOrWhiteSpace($resolvedPublisherId)) {
                    $publisherId = $resolvedPublisherId
                    $publisherName = $resolvedPublisherId
                }
                $keyThumbprint = (($certificate.Thumbprint -replace '\s', '').ToUpperInvariant())
                $certificateSubject = [string]$certificate.Subject
            }
            finally {
                $certificate.Dispose()
            }
        }

        $passwordStorage = $null
        if (Test-Path -LiteralPath $descriptorPath -PathType Leaf) {
            try {
                $descriptorInfo = Read-PackageJsonDocument -Path $descriptorPath
                Assert-PackageSigningPasswordDescriptorSchema -DescriptorInfo $descriptorInfo
                $passwordStorage = [string]$descriptorInfo.Document.protectedPasswordKind
            }
            catch {
                $passwordStorage = 'invalid'
            }
        }

        [pscustomobject]@{
            Name                  = $name
            PublisherId           = $publisherId
            PublisherName         = $publisherName
            PfxPath               = $pfxPath
            CertificatePath       = if (Test-Path -LiteralPath $certificatePath -PathType Leaf) { [System.IO.Path]::GetFullPath($certificatePath) } else { $null }
            SigningDescriptorPath = if (Test-Path -LiteralPath $descriptorPath -PathType Leaf) { $descriptorPath } else { $null }
            KeyThumbprint         = $keyThumbprint
            CertificateSubject    = $certificateSubject
            PasswordStored        = Test-Path -LiteralPath $descriptorPath -PathType Leaf
            PasswordStorage       = $passwordStorage
        }
    }
}

function Get-PackageCertificateCommonName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    $simpleName = $Certificate.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)
    if (-not [string]::IsNullOrWhiteSpace($simpleName)) {
        return $simpleName.Trim()
    }

    $subject = [string]$Certificate.Subject
    if ($subject -match '(?i)(^|,\s*)CN=([^,]+)') {
        return $matches[2].Trim()
    }

    return $null
}

function Get-PackageCertificateDisplayName {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    if (-not [string]::IsNullOrWhiteSpace($Certificate.FriendlyName)) {
        return [string]$Certificate.FriendlyName
    }

    $commonName = Get-PackageCertificateCommonName -Certificate $Certificate
    if (-not [string]::IsNullOrWhiteSpace($commonName)) {
        return $commonName
    }

    return [string]$Certificate.Subject
}

function Resolve-PackagePublisherIdFromCertificate {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    $commonName = Get-PackageCertificateCommonName -Certificate $Certificate
    if ([string]::IsNullOrWhiteSpace($commonName)) {
        return $null
    }

    $candidates = New-Object System.Collections.Generic.List[string]
    $catalogSigningSuffix = ' Package Catalog Signing'
    if ($commonName.EndsWith($catalogSigningSuffix, [System.StringComparison]::OrdinalIgnoreCase)) {
        $candidates.Add($commonName.Substring(0, $commonName.Length - $catalogSigningSuffix.Length).Trim()) | Out-Null
    }
    $candidates.Add($commonName.Trim()) | Out-Null

    foreach ($candidate in @($candidates.ToArray())) {
        try {
            Assert-PackagePublisherId -PublisherId $candidate
            return $candidate
        }
        catch {
            continue
        }
    }

    return $null
}

function New-PackageTrustExportDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry
    )

    return [pscustomobject][ordered]@{
        inventoryVersion = 1
        keys             = @($Entry)
        revokedKeys      = @()
    }
}

function ConvertTo-PackageCertificatePem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    $base64 = [Convert]::ToBase64String($Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert))
    $lines = New-Object System.Collections.Generic.List[string]
    $lines.Add('-----BEGIN CERTIFICATE-----') | Out-Null
    for ($i = 0; $i -lt $base64.Length; $i += 64) {
        $length = [Math]::Min(64, $base64.Length - $i)
        $lines.Add($base64.Substring($i, $length)) | Out-Null
    }
    $lines.Add('-----END CERTIFICATE-----') | Out-Null
    return ($lines.ToArray() -join [Environment]::NewLine)
}

function ConvertFrom-PackageCertificatePem {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$CertificatePem
    )

    $base64 = ($CertificatePem -replace '-----BEGIN CERTIFICATE-----', '' -replace '-----END CERTIFICATE-----', '') -replace '\s', ''
    if ([string]::IsNullOrWhiteSpace($base64)) {
        throw 'Certificate PEM is empty.'
    }

    return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String($base64))
}

function Import-PackageCertificate {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowNull()]
        [securestring]$Password = $null,

        [switch]$WithPrivateKey
    )

    $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
    $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
    if ($WithPrivateKey.IsPresent) {
        $flags = $flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::UserKeySet
    }

    if ($Password) {
        $plainTextPassword = ConvertFrom-PackageSecureString -SecureString $Password
        try {
            return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPath, $plainTextPassword, $flags)
        }
        finally {
            $plainTextPassword = $null
        }
    }

    return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPath)
}

function Get-PackageCertificateRsaPrivateKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    try {
        return [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
    }
    catch {
        if ($Certificate.PrivateKey -is [System.Security.Cryptography.RSA]) {
            return $Certificate.PrivateKey
        }
    }

    return $null
}

function Get-PackageCertificateRsaPublicKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    try {
        return [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPublicKey($Certificate)
    }
    catch {
        if ($Certificate.PublicKey -and $Certificate.PublicKey.Key -is [System.Security.Cryptography.RSA]) {
            return $Certificate.PublicKey.Key
        }
    }

    return $null
}

function Invoke-PackageRsaSignData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.RSA]$Rsa,

        [Parameter(Mandatory = $true)]
        [byte[]]$Bytes
    )

    try {
        return $Rsa.SignData($Bytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
    }
    catch {
        $sha256 = [System.Security.Cryptography.SHA256CryptoServiceProvider]::new()
        try {
            return $Rsa.SignData($Bytes, $sha256)
        }
        finally {
            $sha256.Dispose()
        }
    }
}

function Invoke-PackageRsaVerifyData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.RSA]$Rsa,

        [Parameter(Mandatory = $true)]
        [byte[]]$Bytes,

        [Parameter(Mandatory = $true)]
        [byte[]]$SignatureBytes
    )

    try {
        return $Rsa.VerifyData($Bytes, $SignatureBytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
    }
    catch {
        $sha256 = [System.Security.Cryptography.SHA256CryptoServiceProvider]::new()
        try {
            return $Rsa.VerifyData($Bytes, $sha256, $SignatureBytes)
        }
        finally {
            $sha256.Dispose()
        }
    }
}

function New-PackageTrustEntry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [Parameter(Mandatory = $true)]
        [string]$PublisherId,

        [AllowNull()]
        [string]$PublisherName = $null,

        [AllowNull()]
        [string]$SignerDisplayName = $null,

        [string]$TrustSource = 'userApproved',

        [AllowNull()]
        [string]$TrustReason = $null
    )

    Assert-PackagePublisherId -PublisherId $PublisherId
    if ([string]::IsNullOrWhiteSpace($PublisherName)) {
        $PublisherName = $PublisherId
    }
    if ([string]::IsNullOrWhiteSpace($SignerDisplayName)) {
        $SignerDisplayName = Get-PackageCertificateDisplayName -Certificate $Certificate
    }

    $entry = [ordered]@{
        publisherId              = $PublisherId
        publisherName            = $PublisherName
        keyThumbprint            = (($Certificate.Thumbprint -replace '\s', '').ToUpperInvariant())
        certificatePem           = ConvertTo-PackageCertificatePem -Certificate $Certificate
        certificateSubject       = [string]$Certificate.Subject
        certificateIssuer        = [string]$Certificate.Issuer
        certificateSerialNumber  = [string]$Certificate.SerialNumber
        notBeforeUtc             = $Certificate.NotBefore.ToUniversalTime().ToString('o')
        notAfterUtc              = $Certificate.NotAfter.ToUniversalTime().ToString('o')
        signerDisplayName        = $SignerDisplayName
        trustSource              = $TrustSource
        trustedAtUtc             = [DateTime]::UtcNow.ToString('o')
        trustedBy                = [Environment]::UserName
        enabled                  = $true
    }
    if (-not [string]::IsNullOrWhiteSpace($TrustReason)) {
        $entry['trustReason'] = $TrustReason
    }

    return [pscustomobject]$entry
}

function Get-PackageTrustEntries {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Document
    )

    if (-not $Document.PSObject.Properties['keys'] -or $null -eq $Document.keys) {
        return @()
    }
    if ($Document.keys -isnot [System.Array]) {
        throw 'Package trust inventory must define keys as an array.'
    }

    return @($Document.keys)
}

function Get-PackageRevokedKeyEntries {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Document
    )

    if (-not $Document.PSObject.Properties['revokedKeys'] -or $null -eq $Document.revokedKeys) {
        return @()
    }
    if ($Document.revokedKeys -isnot [System.Array]) {
        throw 'Package trust inventory must define revokedKeys as an array.'
    }

    return @($Document.revokedKeys)
}

function Assert-PackageTrustInventorySchema {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$TrustInventoryDocumentInfo
    )

    $document = $TrustInventoryDocumentInfo.Document
    if (-not $document.PSObject.Properties['inventoryVersion']) {
        throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' is missing inventoryVersion."
    }
    if (-not $document.PSObject.Properties['keys']) {
        throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' is missing keys."
    }
    if (-not $document.PSObject.Properties['revokedKeys']) {
        throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' is missing revokedKeys."
    }

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($entry in @(Get-PackageTrustEntries -Document $document)) {
        foreach ($requiredProperty in @('publisherId', 'publisherName', 'keyThumbprint', 'certificatePem', 'trustSource', 'trustedAtUtc', 'enabled')) {
            if (-not $entry.PSObject.Properties[$requiredProperty] -or
                ($requiredProperty -ne 'enabled' -and [string]::IsNullOrWhiteSpace([string]$entry.$requiredProperty))) {
                throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' has a key entry missing '$requiredProperty'."
            }
        }
        Assert-PackagePublisherId -PublisherId ([string]$entry.publisherId)
        $thumbprint = ([string]$entry.keyThumbprint).Trim().ToUpperInvariant()
        if ($thumbprint -notmatch '^[A-F0-9]{40,128}$') {
            throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' has invalid keyThumbprint '$($entry.keyThumbprint)'."
        }
        if (-not $seen.Add($thumbprint)) {
            throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' defines duplicate keyThumbprint '$thumbprint'."
        }
    }

    foreach ($entry in @(Get-PackageRevokedKeyEntries -Document $document)) {
        if (-not $entry.PSObject.Properties['keyThumbprint'] -or [string]::IsNullOrWhiteSpace([string]$entry.keyThumbprint)) {
            throw "Package trust inventory '$($TrustInventoryDocumentInfo.Path)' has a revokedKeys entry missing keyThumbprint."
        }
    }
}

function Get-PackageTrustInventoryInfo {
    [CmdletBinding()]
    param()

    $inventoryPath = Get-PackageTrustInventoryPath
    $documentInfo = Read-PackageJsonDocument -Path $inventoryPath
    Assert-PackageTrustInventorySchema -TrustInventoryDocumentInfo $documentInfo
    $documentInfo | Add-Member -MemberType NoteProperty -Name Exists -Value $true -Force
    return $documentInfo
}

function Get-PackageTrustInventoryEditInfo {
    [CmdletBinding()]
    param()

    $documentInfo = Get-PackageTrustInventoryInfo
    if (-not $documentInfo.Document.PSObject.Properties['keys'] -or $null -eq $documentInfo.Document.keys) {
        Set-PackageObjectProperty -InputObject $documentInfo.Document -Name 'keys' -Value @()
    }
    if (-not $documentInfo.Document.PSObject.Properties['revokedKeys'] -or $null -eq $documentInfo.Document.revokedKeys) {
        Set-PackageObjectProperty -InputObject $documentInfo.Document -Name 'revokedKeys' -Value @()
    }
    return $documentInfo
}

function Save-PackageTrustInventoryDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$DocumentInfo
    )

    Assert-PackageTrustInventorySchema -TrustInventoryDocumentInfo $DocumentInfo
    Save-PackageJsonDocument -Path $DocumentInfo.Path -Document $DocumentInfo.Document
}

function Get-PackageTrustEntryByThumbprint {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Document,

        [Parameter(Mandatory = $true)]
        [string]$KeyThumbprint
    )

    $normalized = (($KeyThumbprint -replace '\s', '').ToUpperInvariant())
    foreach ($entry in @(Get-PackageTrustEntries -Document $Document)) {
        if ([string]::Equals((([string]$entry.keyThumbprint -replace '\s', '').ToUpperInvariant()), $normalized, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $entry
        }
    }
    return $null
}

function Test-PackageKeyThumbprintRevoked {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$TrustInventoryDocument,

        [Parameter(Mandatory = $true)]
        [string]$KeyThumbprint,

        [AllowNull()]
        [string]$PublisherId = $null
    )

    $normalized = (($KeyThumbprint -replace '\s', '').ToUpperInvariant())
    foreach ($revoked in @(Get-PackageRevokedKeyEntries -Document $TrustInventoryDocument)) {
        $revokedThumbprint = (([string]$revoked.keyThumbprint -replace '\s', '').ToUpperInvariant())
        if (-not [string]::Equals($revokedThumbprint, $normalized, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }
        if (-not [string]::IsNullOrWhiteSpace($PublisherId) -and
            $revoked.PSObject.Properties['publisherId'] -and
            -not [string]::IsNullOrWhiteSpace([string]$revoked.publisherId) -and
            -not [string]::Equals([string]$revoked.publisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }
        return $true
    }
    return $false
}

function Select-PackageTrustSummary {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Entry,

        [Parameter(Mandatory = $true)]
        [string]$InventoryPath
    )

    return [pscustomobject]@{
        PublisherId        = [string]$Entry.publisherId
        PublisherName      = [string]$Entry.publisherName
        KeyThumbprint      = [string]$Entry.keyThumbprint
        SignerDisplayName  = if ($Entry.PSObject.Properties['signerDisplayName']) { [string]$Entry.signerDisplayName } else { $null }
        CertificateSubject = if ($Entry.PSObject.Properties['certificateSubject']) { [string]$Entry.certificateSubject } else { $null }
        NotBeforeUtc       = if ($Entry.PSObject.Properties['notBeforeUtc']) { [string]$Entry.notBeforeUtc } else { $null }
        NotAfterUtc        = if ($Entry.PSObject.Properties['notAfterUtc']) { [string]$Entry.notAfterUtc } else { $null }
        TrustSource        = [string]$Entry.trustSource
        TrustReason        = if ($Entry.PSObject.Properties['trustReason']) { [string]$Entry.trustReason } else { $null }
        TrustedAtUtc       = [string]$Entry.trustedAtUtc
        Enabled            = [bool]$Entry.enabled
        RevokedAtUtc       = if ($Entry.PSObject.Properties['revokedAtUtc']) { [string]$Entry.revokedAtUtc } else { $null }
        RevocationReason   = if ($Entry.PSObject.Properties['revocationReason']) { [string]$Entry.revocationReason } else { $null }
        InventoryPath      = $InventoryPath
    }
}

function Get-PackageTrustSummaries {
    [CmdletBinding()]
    param()

    $documentInfo = Get-PackageTrustInventoryEditInfo
    foreach ($entry in @(Get-PackageTrustEntries -Document $documentInfo.Document)) {
        Select-PackageTrustSummary -Entry $entry -InventoryPath $documentInfo.Path
    }
}

function Set-PackageDefinitionUnsignedSignature {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition
    )

    if (-not $Definition.PSObject.Properties['definitionPublication'] -or -not $Definition.definitionPublication) {
        throw 'Package definition is missing definitionPublication.'
    }
    $signature = [pscustomobject][ordered]@{
        kind          = 'unsigned'
        format        = $script:PackageDefinitionSignatureFormat
        signedContent = $script:PackageDefinitionSignedContentKind
    }
    Set-PackageObjectProperty -InputObject $Definition.definitionPublication -Name 'definitionSignature' -Value $signature
}

function Set-PackageDefinitionSignature {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition,

        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$SignatureValue
    )

    if (-not $Definition.PSObject.Properties['definitionPublication'] -or -not $Definition.definitionPublication) {
        throw 'Package definition is missing definitionPublication.'
    }

    $signerDisplayName = Get-PackageCertificateDisplayName -Certificate $Certificate
    $signature = [pscustomobject][ordered]@{
        kind               = 'signed'
        format             = $script:PackageDefinitionSignatureFormat
        signedContent      = $script:PackageDefinitionSignedContentKind
        keyThumbprint      = (($Certificate.Thumbprint -replace '\s', '').ToUpperInvariant())
        signerDisplayName  = $signerDisplayName
        certificateSubject = [string]$Certificate.Subject
        certificatePem     = ConvertTo-PackageCertificatePem -Certificate $Certificate
        signedAtUtc        = [DateTime]::UtcNow.ToString('o')
        signatureValue     = $SignatureValue
    }
    Set-PackageObjectProperty -InputObject $Definition.definitionPublication -Name 'definitionSignature' -Value $signature
}

function Invoke-PackageDefinitionDocumentSigning {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition,

        [Parameter(Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
    )

    Set-PackageDefinitionSignature -Definition $Definition -Certificate $Certificate -SignatureValue ''
    $signable = Get-PackageDefinitionSignableContent -Definition $Definition
    $rsa = Get-PackageCertificateRsaPrivateKey -Certificate $Certificate
    if (-not $rsa) {
        throw 'Signing certificate does not contain an RSA private key.'
    }
    try {
        $signatureBytes = Invoke-PackageRsaSignData -Rsa $rsa -Bytes $signable.Bytes
    }
    finally {
        $rsa.Dispose()
    }
    Set-PackageObjectProperty -InputObject $Definition.definitionPublication.definitionSignature -Name 'signatureValue' -Value ([Convert]::ToBase64String($signatureBytes))

    return [pscustomobject]@{
        KeyThumbprint        = (($Certificate.Thumbprint -replace '\s', '').ToUpperInvariant())
        CanonicalContentHash = $signable.Sha256
        SignatureValue       = [Convert]::ToBase64String($signatureBytes)
    }
}

function Test-PackageDefinitionSignatureDocument {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$Definition,

        [AllowNull()]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate = $null,

        [AllowNull()]
        [psobject]$TrustInventoryDocument = $null
    )

    $publication = if ($Definition.PSObject.Properties['definitionPublication']) { $Definition.definitionPublication } else { $null }
    $signature = if ($publication -and $publication.PSObject.Properties['definitionSignature']) { $publication.definitionSignature } else { $null }
    if (-not $signature) {
        return [pscustomobject]@{
            Status               = 'missingSignature'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = $null
            CanonicalContentHash = $null
            ErrorMessage         = 'definitionPublication.definitionSignature is missing.'
        }
    }
    if ([string]::Equals([string]$signature.kind, 'unsigned', [System.StringComparison]::OrdinalIgnoreCase)) {
        return [pscustomobject]@{
            Status               = 'unsigned'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = $null
            CanonicalContentHash = (Get-PackageDefinitionSignableContent -Definition $Definition).Sha256
            ErrorMessage         = $null
        }
    }
    if (-not [string]::Equals([string]$signature.kind, 'signed', [System.StringComparison]::OrdinalIgnoreCase)) {
        return [pscustomobject]@{
            Status               = 'unsupportedSignatureKind'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = $null
            CanonicalContentHash = $null
            ErrorMessage         = "Unsupported definitionSignature.kind '$($signature.kind)'."
        }
    }
    if (-not [string]::Equals([string]$signature.format, $script:PackageDefinitionSignatureFormat, [System.StringComparison]::Ordinal)) {
        return [pscustomobject]@{
            Status               = 'unsupportedSignatureFormat'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = [string]$signature.keyThumbprint
            CanonicalContentHash = $null
            ErrorMessage         = "Unsupported definitionSignature.format '$($signature.format)'."
        }
    }
    if (-not $signature.PSObject.Properties['signatureValue'] -or [string]::IsNullOrWhiteSpace([string]$signature.signatureValue)) {
        return [pscustomobject]@{
            Status               = 'missingSignatureValue'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = [string]$signature.keyThumbprint
            CanonicalContentHash = $null
            ErrorMessage         = 'definitionSignature.signatureValue is missing.'
        }
    }

    $keyThumbprint = (([string]$signature.keyThumbprint -replace '\s', '').ToUpperInvariant())
    $publisherId = if ($publication -and $publication.PSObject.Properties['publisherId']) { [string]$publication.publisherId } else { $null }
    $embeddedCertificatePem = if ($signature.PSObject.Properties['certificatePem']) { [string]$signature.certificatePem } else { $null }
    $embeddedCertificate = $null
    if (-not [string]::IsNullOrWhiteSpace($embeddedCertificatePem)) {
        try {
            $embeddedCertificate = ConvertFrom-PackageCertificatePem -CertificatePem $embeddedCertificatePem
        }
        catch {
            return [pscustomobject]@{
                Status               = 'invalidEmbeddedCertificate'
                Valid                = $false
                Trusted              = $false
                KeyThumbprint        = $keyThumbprint
                CanonicalContentHash = $null
                CertificatePem       = $embeddedCertificatePem
                ErrorMessage         = 'definitionSignature.certificatePem is not a valid certificate PEM.'
            }
        }

        $embeddedThumbprint = (($embeddedCertificate.Thumbprint -replace '\s', '').ToUpperInvariant())
        if (-not [string]::Equals($embeddedThumbprint, $keyThumbprint, [System.StringComparison]::OrdinalIgnoreCase)) {
            return [pscustomobject]@{
                Status               = 'certificateThumbprintMismatch'
                Valid                = $false
                Trusted              = $false
                KeyThumbprint        = $keyThumbprint
                CanonicalContentHash = $null
                SignerDisplayName    = if ($signature.PSObject.Properties['signerDisplayName']) { [string]$signature.signerDisplayName } else { $null }
                CertificateSubject   = [string]$embeddedCertificate.Subject
                CertificatePem       = $embeddedCertificatePem
                CertificateNotBeforeUtc = $embeddedCertificate.NotBefore.ToUniversalTime().ToString('o')
                CertificateNotAfterUtc = $embeddedCertificate.NotAfter.ToUniversalTime().ToString('o')
                ErrorMessage         = "definitionSignature.certificatePem thumbprint '$embeddedThumbprint' does not match keyThumbprint '$keyThumbprint'."
            }
        }
    }

    $trustEntry = $null
    $trusted = $false
    $revoked = $false
    $certificateSource = if ($Certificate) { 'parameter' } else { $null }
    $trustEntryFound = $false
    $trustEntryPublisherMatches = $false
    if ($TrustInventoryDocument) {
        $revoked = Test-PackageKeyThumbprintRevoked -TrustInventoryDocument $TrustInventoryDocument -KeyThumbprint $keyThumbprint -PublisherId $publisherId
        $trustEntry = Get-PackageTrustEntryByThumbprint -Document $TrustInventoryDocument -KeyThumbprint $keyThumbprint
        $trustEntryFound = $null -ne $trustEntry
        if ($trustEntry -and $trustEntry.PSObject.Properties['publisherId'] -and
            [string]::Equals([string]$trustEntry.publisherId, [string]$publisherId, [System.StringComparison]::OrdinalIgnoreCase)) {
            $trustEntryPublisherMatches = $true
        }
        $trustEntryRevokedAtUtc = if ($trustEntry -and $trustEntry.PSObject.Properties['revokedAtUtc']) { [string]$trustEntry.revokedAtUtc } else { $null }
        if ($trustEntry -and [bool]$trustEntry.enabled -and $trustEntryPublisherMatches -and [string]::IsNullOrWhiteSpace($trustEntryRevokedAtUtc)) {
            $trusted = $true
        }
        if ($trustEntry -and -not $Certificate) {
            $Certificate = ConvertFrom-PackageCertificatePem -CertificatePem ([string]$trustEntry.certificatePem)
            $certificateSource = 'trustInventory'
        }
    }

    if ($revoked) {
        return [pscustomobject]@{
            Status               = 'revokedKey'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = $keyThumbprint
            CanonicalContentHash = $null
            ErrorMessage         = "Definition signing key '$keyThumbprint' is revoked."
        }
    }
    if (-not $Certificate) {
        if ($embeddedCertificate) {
            $Certificate = $embeddedCertificate
            $certificateSource = 'embedded'
        }
    }

    if (-not $Certificate) {
        return [pscustomobject]@{
            Status               = 'unknownKey'
            Valid                = $false
            Trusted              = $false
            KeyThumbprint        = $keyThumbprint
            CanonicalContentHash = $null
            CertificatePem       = $embeddedCertificatePem
            ErrorMessage         = "No certificate was provided or trusted for key '$keyThumbprint'."
        }
    }

    $signable = Get-PackageDefinitionSignableContent -Definition $Definition
    try {
        $signatureBytes = [Convert]::FromBase64String([string]$signature.signatureValue)
    }
    catch {
        return [pscustomobject]@{
            Status               = 'invalidSignatureValue'
            Valid                = $false
            Trusted              = $trusted
            KeyThumbprint        = $keyThumbprint
            CanonicalContentHash = $signable.Sha256
            ErrorMessage         = 'definitionSignature.signatureValue is not valid base64.'
        }
    }

    $rsa = Get-PackageCertificateRsaPublicKey -Certificate $Certificate
    if (-not $rsa) {
        throw 'Verification certificate does not contain an RSA public key.'
    }
    try {
        $valid = Invoke-PackageRsaVerifyData -Rsa $rsa -Bytes $signable.Bytes -SignatureBytes $signatureBytes
    }
    finally {
        $rsa.Dispose()
    }

    return [pscustomobject]@{
        Status               = if ($valid) { if ($trusted) { 'validTrusted' } else { 'validUntrusted' } } else { 'invalidSignature' }
        Valid                = [bool]$valid
        Trusted              = [bool]($valid -and $trusted)
        KeyThumbprint        = $keyThumbprint
        CanonicalContentHash = $signable.Sha256
        SignerDisplayName    = if ($signature.PSObject.Properties['signerDisplayName']) { [string]$signature.signerDisplayName } else { $null }
        CertificateSubject   = [string]$Certificate.Subject
        CertificatePem       = $embeddedCertificatePem
        CertificateSource    = $certificateSource
        CertificateNotBeforeUtc = $Certificate.NotBefore.ToUniversalTime().ToString('o')
        CertificateNotAfterUtc = $Certificate.NotAfter.ToUniversalTime().ToString('o')
        TrustEntryFound      = [bool]$trustEntryFound
        TrustEntryPublisherMatches = [bool]$trustEntryPublisherMatches
        ErrorMessage         = if ($valid) { $null } else { 'Signature verification failed.' }
    }
}