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-PackageJsonEscapedString { [CmdletBinding()] param( [AllowNull()] [string]$Value ) if ($null -eq $Value) { return '""' } $builder = [System.Text.StringBuilder]::new() $null = $builder.Append('"') foreach ($ch in $Value.ToCharArray()) { $code = [int][char]$ch switch ($code) { 8 { $null = $builder.Append('\b'); continue } 9 { $null = $builder.Append('\t'); continue } 10 { $null = $builder.Append('\n'); continue } 12 { $null = $builder.Append('\f'); continue } 13 { $null = $builder.Append('\r'); continue } 34 { $null = $builder.Append('\"'); continue } 92 { $null = $builder.Append('\\'); continue } } if ($code -lt 32) { $null = $builder.Append(('\u{0:x4}' -f $code)) continue } $null = $builder.Append($ch) } $null = $builder.Append('"') return $builder.ToString() } 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] -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 Save-PackageJsonDocument { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [psobject]$Document ) $resolvedPath = [System.IO.Path]::GetFullPath($Path) $directory = Split-Path -Parent $resolvedPath if (-not [string]::IsNullOrWhiteSpace($directory)) { $null = New-Item -ItemType Directory -Path $directory -Force } $temporaryPath = '{0}.{1}.tmp' -f $resolvedPath, ([guid]::NewGuid().ToString('N')) try { $Document | ConvertTo-Json -Depth 80 | Set-Content -LiteralPath $temporaryPath -Encoding UTF8 Move-Item -LiteralPath $temporaryPath -Destination $resolvedPath -Force } finally { if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) { Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue } } } 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 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-PackageSigningProfileInventoryPath { [CmdletBinding()] param() return [System.IO.Path]::GetFullPath((Join-Path (Join-Path (Get-PackageLocalRoot) 'Configuration\Private') 'PackageSigningProfiles.json')) } function New-PackageSigningProfileInventoryDocument { [CmdletBinding()] param() return [pscustomobject][ordered]@{ inventoryVersion = 1 profiles = @() } } function Get-PackageSigningProfileEntries { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Document ) if (-not $Document.PSObject.Properties['profiles'] -or $null -eq $Document.profiles) { return @() } if ($Document.profiles -isnot [System.Array]) { throw 'Package signing profile inventory must define profiles as an array.' } return @($Document.profiles) } function Assert-PackageSigningProfileInventorySchema { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$ProfileInventoryDocumentInfo ) $document = $ProfileInventoryDocumentInfo.Document if (-not $document.PSObject.Properties['inventoryVersion']) { throw "Package signing profile inventory '$($ProfileInventoryDocumentInfo.Path)' is missing inventoryVersion." } if (-not $document.PSObject.Properties['profiles']) { throw "Package signing profile inventory '$($ProfileInventoryDocumentInfo.Path)' is missing profiles." } foreach ($profile in @(Get-PackageSigningProfileEntries -Document $document)) { foreach ($requiredProperty in @('name', 'publisherId', 'pfxPath', 'keyThumbprint', 'protectedPassword', 'protectedPasswordKind')) { if (-not $profile.PSObject.Properties[$requiredProperty] -or [string]::IsNullOrWhiteSpace([string]$profile.$requiredProperty)) { throw "Package signing profile inventory '$($ProfileInventoryDocumentInfo.Path)' has a profile entry missing '$requiredProperty'." } } Assert-PackagePublisherId -PublisherId ([string]$profile.publisherId) } } function Get-PackageSigningProfileInventoryInfo { [CmdletBinding()] param() $profilePath = Get-PackageSigningProfileInventoryPath if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf)) { return [pscustomobject]@{ Path = $profilePath Document = New-PackageSigningProfileInventoryDocument Exists = $false } } $documentInfo = Read-PackageJsonDocument -Path $profilePath Assert-PackageSigningProfileInventorySchema -ProfileInventoryDocumentInfo $documentInfo $documentInfo | Add-Member -MemberType NoteProperty -Name Exists -Value $true -Force return $documentInfo } function Save-PackageSigningProfileInventoryDocument { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$DocumentInfo ) Assert-PackageSigningProfileInventorySchema -ProfileInventoryDocumentInfo $DocumentInfo Save-PackageJsonDocument -Path $DocumentInfo.Path -Document $DocumentInfo.Document } 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 Select-PackageSigningProfileSummary { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$Profile, [Parameter(Mandatory = $true)] [string]$ProfileInventoryPath ) return [pscustomobject]@{ Name = [string]$Profile.name PublisherId = [string]$Profile.publisherId PublisherName = if ($Profile.PSObject.Properties['publisherName']) { [string]$Profile.publisherName } else { $null } PfxPath = [string]$Profile.pfxPath CertificatePath = if ($Profile.PSObject.Properties['certificatePath']) { [string]$Profile.certificatePath } else { $null } TrustExportPath = if ($Profile.PSObject.Properties['trustExportPath']) { [string]$Profile.trustExportPath } else { $null } KeyThumbprint = [string]$Profile.keyThumbprint CertificateSubject = if ($Profile.PSObject.Properties['certificateSubject']) { [string]$Profile.certificateSubject } else { $null } CreatedAtUtc = if ($Profile.PSObject.Properties['createdAtUtc']) { [string]$Profile.createdAtUtc } else { $null } UpdatedAtUtc = if ($Profile.PSObject.Properties['updatedAtUtc']) { [string]$Profile.updatedAtUtc } else { $null } LastUsedAtUtc = if ($Profile.PSObject.Properties['lastUsedAtUtc']) { [string]$Profile.lastUsedAtUtc } else { $null } IsDefault = if ($Profile.PSObject.Properties['isDefault']) { [bool]$Profile.isDefault } else { $false } PasswordStored = -not [string]::IsNullOrWhiteSpace([string]$Profile.protectedPassword) PasswordStorage = if ($Profile.PSObject.Properties['protectedPasswordKind']) { [string]$Profile.protectedPasswordKind } else { $null } ProfileInventoryPath = $ProfileInventoryPath } } function Get-PackageSigningProfileSummaries { [CmdletBinding()] param() $documentInfo = Get-PackageSigningProfileInventoryInfo foreach ($profile in @(Get-PackageSigningProfileEntries -Document $documentInfo.Document)) { Select-PackageSigningProfileSummary -Profile $profile -ProfileInventoryPath $documentInfo.Path } } function Get-PackageSigningProfileByPublisherId { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PublisherId ) $documentInfo = Get-PackageSigningProfileInventoryInfo $matches = @( Get-PackageSigningProfileEntries -Document $documentInfo.Document | Where-Object { [string]::Equals([string]$_.publisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase) } ) if ($matches.Count -eq 0) { return $null } $selected = @($matches | Sort-Object -Property ` @{ Expression = { if ($_.PSObject.Properties['isDefault'] -and [bool]$_.isDefault) { 1 } else { 0 } } }, ` @{ Expression = { if ($_.PSObject.Properties['lastUsedAtUtc']) { [string]$_.lastUsedAtUtc } else { '' } } }, ` @{ Expression = { if ($_.PSObject.Properties['updatedAtUtc']) { [string]$_.updatedAtUtc } else { '' } } } ` -Descending | Select-Object -First 1) return $selected[0] } function Get-PackageSigningProfileByPfxPath { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PfxPath ) $resolvedPath = [System.IO.Path]::GetFullPath($PfxPath) $documentInfo = Get-PackageSigningProfileInventoryInfo foreach ($profile in @(Get-PackageSigningProfileEntries -Document $documentInfo.Document)) { if ($profile.PSObject.Properties['pfxPath'] -and [string]::Equals([System.IO.Path]::GetFullPath([string]$profile.pfxPath), $resolvedPath, [System.StringComparison]::OrdinalIgnoreCase)) { return $profile } } return $null } function Set-PackageSigningProfile { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$PublisherId, [AllowNull()] [string]$PublisherName = $null, [Parameter(Mandatory = $true)] [string]$PfxPath, [AllowNull()] [string]$CertificatePath = $null, [AllowNull()] [string]$TrustExportPath = $null, [Parameter(Mandatory = $true)] [string]$KeyThumbprint, [AllowNull()] [string]$CertificateSubject = $null, [Parameter(Mandatory = $true)] [securestring]$Password ) Assert-PackagePublisherId -PublisherId $PublisherId $documentInfo = Get-PackageSigningProfileInventoryInfo $now = [DateTime]::UtcNow.ToString('o') $protectedPassword = Protect-PackageSigningProfilePassword -Password $Password $profiles = New-Object System.Collections.Generic.List[object] $existingCreatedAtUtc = $null foreach ($profile in @(Get-PackageSigningProfileEntries -Document $documentInfo.Document)) { if ([string]::Equals([string]$profile.publisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase)) { if ($profile.PSObject.Properties['createdAtUtc']) { $existingCreatedAtUtc = [string]$profile.createdAtUtc } continue } $profiles.Add($profile) | Out-Null } $profile = [pscustomobject][ordered]@{ name = $Name publisherId = $PublisherId publisherName = if ([string]::IsNullOrWhiteSpace($PublisherName)) { $PublisherId } else { $PublisherName } pfxPath = [System.IO.Path]::GetFullPath($PfxPath) certificatePath = if ([string]::IsNullOrWhiteSpace($CertificatePath)) { $null } else { [System.IO.Path]::GetFullPath($CertificatePath) } trustExportPath = if ([string]::IsNullOrWhiteSpace($TrustExportPath)) { $null } else { [System.IO.Path]::GetFullPath($TrustExportPath) } keyThumbprint = (($KeyThumbprint -replace '\s', '').ToUpperInvariant()) certificateSubject = $CertificateSubject protectedPassword = $protectedPassword protectedPasswordKind = 'dpapi-current-user-securestring' createdAtUtc = if ([string]::IsNullOrWhiteSpace($existingCreatedAtUtc)) { $now } else { $existingCreatedAtUtc } updatedAtUtc = $now lastUsedAtUtc = $null isDefault = $true userName = [Environment]::UserName machineName = [Environment]::MachineName } $profiles.Add($profile) | Out-Null Set-PackageObjectProperty -InputObject $documentInfo.Document -Name 'profiles' -Value @($profiles.ToArray()) Save-PackageSigningProfileInventoryDocument -DocumentInfo $documentInfo return Select-PackageSigningProfileSummary -Profile $profile -ProfileInventoryPath $documentInfo.Path } function Set-PackageSigningProfileLastUsed { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$PublisherId, [Parameter(Mandatory = $true)] [string]$KeyThumbprint ) $documentInfo = Get-PackageSigningProfileInventoryInfo $normalizedThumbprint = (($KeyThumbprint -replace '\s', '').ToUpperInvariant()) $now = [DateTime]::UtcNow.ToString('o') $changed = $false foreach ($profile in @(Get-PackageSigningProfileEntries -Document $documentInfo.Document)) { if (-not [string]::Equals([string]$profile.publisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase)) { continue } $isSelected = [string]::Equals((([string]$profile.keyThumbprint -replace '\s', '').ToUpperInvariant()), $normalizedThumbprint, [System.StringComparison]::OrdinalIgnoreCase) if ($isSelected) { Set-PackageObjectProperty -InputObject $profile -Name 'lastUsedAtUtc' -Value $now Set-PackageObjectProperty -InputObject $profile -Name 'updatedAtUtc' -Value $now } Set-PackageObjectProperty -InputObject $profile -Name 'isDefault' -Value $isSelected $changed = $true } if ($changed) { Save-PackageSigningProfileInventoryDocument -DocumentInfo $documentInfo } } 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 Resolve-PackagePublisherIdFromCertificate { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate ) $candidate = Get-PackageCertificateCommonName -Certificate $Certificate if ([string]::IsNullOrWhiteSpace($candidate)) { return $null } try { Assert-PackagePublisherId -PublisherId $candidate return $candidate } catch { 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 = if ([string]::IsNullOrWhiteSpace($Certificate.FriendlyName)) { $Certificate.Subject } else { $Certificate.FriendlyName } } $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 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 = if ([string]::IsNullOrWhiteSpace($Certificate.FriendlyName)) { $Certificate.Subject } else { $Certificate.FriendlyName } $signature = [pscustomobject][ordered]@{ kind = 'signed' format = $script:PackageDefinitionSignatureFormat signedContent = $script:PackageDefinitionSignedContentKind keyThumbprint = (($Certificate.Thumbprint -replace '\s', '').ToUpperInvariant()) signerDisplayName = $signerDisplayName certificateSubject = [string]$Certificate.Subject 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 } $trustEntry = $null $trusted = $false $revoked = $false if ($TrustInventoryDocument) { $revoked = Test-PackageKeyThumbprintRevoked -TrustInventoryDocument $TrustInventoryDocument -KeyThumbprint $keyThumbprint -PublisherId $publisherId $trustEntry = Get-PackageTrustEntryByThumbprint -Document $TrustInventoryDocument -KeyThumbprint $keyThumbprint $trustEntryPublisherMatches = $false 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) } } if ($revoked) { return [pscustomobject]@{ Status = 'revokedKey' Valid = $false Trusted = $false KeyThumbprint = $keyThumbprint CanonicalContentHash = $null ErrorMessage = "Definition signing key '$keyThumbprint' is revoked." } } if (-not $Certificate) { return [pscustomobject]@{ Status = 'unknownKey' Valid = $false Trusted = $false KeyThumbprint = $keyThumbprint CanonicalContentHash = $null 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 ErrorMessage = if ($valid) { $null } else { 'Signature verification failed.' } } } |