Commands/Trust/Eigenverft.Manifested.Package.Cmd.PackageTrust.ps1
|
<#
Public package catalog-signature and trust management surface. #> function Add-PackageTrustCommandMessages { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [psobject]$InputObject, [string[]]$Messages = @(), [string[]]$NextSteps = @() ) process { foreach ($message in @($Messages)) { if (-not [string]::IsNullOrWhiteSpace($message)) { Write-PackageExecutionMessage -Message ("[ACTION] {0}" -f $message) } } foreach ($nextStep in @($NextSteps)) { if (-not [string]::IsNullOrWhiteSpace($nextStep)) { Write-PackageExecutionMessage -Message ("[NEXT] {0}" -f $nextStep) } } $InputObject | Add-Member -MemberType NoteProperty -Name 'Messages' -Value @($Messages) -Force $InputObject | Add-Member -MemberType NoteProperty -Name 'NextSteps' -Value @($NextSteps) -Force return $InputObject } } function Get-PackageDefinitionPublisherIdForSigning { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [psobject]$DefinitionInfo ) $document = $DefinitionInfo.Document if (-not $document.PSObject.Properties['definitionPublication'] -or -not $document.definitionPublication -or -not $document.definitionPublication.PSObject.Properties['publisherId'] -or [string]::IsNullOrWhiteSpace([string]$document.definitionPublication.publisherId)) { throw "Package definition '$($DefinitionInfo.Path)' is missing definitionPublication.publisherId. Set it to the publisher id used by New-PackageSigningCertificate." } return [string]$document.definitionPublication.publisherId } function New-PackageSigningCertificate { [CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateNotNullOrEmpty()] [string]$Name = $null, [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$PublisherName = $null, [AllowNull()] [string]$OutputDirectory = $null, [AllowNull()] [string]$PfxPath = $null, [AllowNull()] [string]$CertificatePath = $null, [AllowNull()] [string]$TrustExportPath = $null, [Parameter(Mandatory = $true)] [securestring]$Password, [AllowNull()] [string]$Subject = $null, [ValidateRange(2048, 16384)] [int]$KeyLength = 3072, [ValidateRange(1, 50)] [int]$ValidYears = 10 ) if (-not ([type]'System.Security.Cryptography.X509Certificates.CertificateRequest')) { throw 'This PowerShell/.NET runtime does not expose CertificateRequest. Create a PFX externally or run this command on PowerShell 7+.' } $usesFriendlyProfile = -not [string]::IsNullOrWhiteSpace($Name) if (-not $usesFriendlyProfile -and [string]::IsNullOrWhiteSpace($PfxPath)) { throw "Use either -Name for the friendly signing-profile workflow or -PfxPath for an explicit PFX path." } if ([string]::IsNullOrWhiteSpace($PublisherId)) { $PublisherId = if ($usesFriendlyProfile) { $Name } else { $null } } if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { Assert-PackagePublisherId -PublisherId $PublisherId } if ([string]::IsNullOrWhiteSpace($PublisherName) -and -not [string]::IsNullOrWhiteSpace($PublisherId)) { $PublisherName = $PublisherId } if ([string]::IsNullOrWhiteSpace($Subject)) { $Subject = if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { "CN=$PublisherId" } else { 'CN=Eigenverft Package Catalog Signing' } } $safeName = if ($usesFriendlyProfile) { ConvertTo-PackageSafeFileName -Value $Name } else { $null } if ($usesFriendlyProfile) { $baseDirectory = if ([string]::IsNullOrWhiteSpace($OutputDirectory)) { Get-PackageDefaultSigningDirectory } else { [System.IO.Path]::GetFullPath($OutputDirectory) } $profileDirectory = Join-Path $baseDirectory $safeName if ([string]::IsNullOrWhiteSpace($PfxPath)) { $PfxPath = Join-Path $profileDirectory ("{0}.catalog-signing.pfx" -f $safeName) } if ([string]::IsNullOrWhiteSpace($CertificatePath)) { $CertificatePath = Join-Path $profileDirectory ("{0}.catalog-signing.cer" -f $safeName) } if ([string]::IsNullOrWhiteSpace($TrustExportPath)) { $TrustExportPath = Join-Path $profileDirectory ("{0}.package-trust.json" -f $safeName) } } $resolvedPfxPath = [System.IO.Path]::GetFullPath($PfxPath) $resolvedCertificatePath = if ([string]::IsNullOrWhiteSpace($CertificatePath)) { $null } else { [System.IO.Path]::GetFullPath($CertificatePath) } $resolvedTrustExportPath = if ([string]::IsNullOrWhiteSpace($TrustExportPath)) { $null } else { [System.IO.Path]::GetFullPath($TrustExportPath) } $rsa = [System.Security.Cryptography.RSA]::Create($KeyLength) $certificate = $null try { $request = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( $Subject, $rsa, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 ) $request.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($false, $false, 0, $true)) | Out-Null $request.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new([System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature, $true)) | Out-Null $request.CertificateExtensions.Add([System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new($request.PublicKey, $false)) | Out-Null $notBefore = [DateTimeOffset]::UtcNow.AddDays(-1) $notAfter = [DateTimeOffset]::UtcNow.AddYears($ValidYears) $certificate = $request.CreateSelfSigned($notBefore, $notAfter) try { $certificate.FriendlyName = if ($usesFriendlyProfile) { "$PublisherId Package Catalog Signing" } else { 'Eigenverft Package Catalog Signing' } } catch { # FriendlyName is not writable on every platform/runtime. } $thumbprint = (($certificate.Thumbprint -replace '\s', '').ToUpperInvariant()) $messages = New-Object System.Collections.Generic.List[string] $nextSteps = New-Object System.Collections.Generic.List[string] if ($PSCmdlet.ShouldProcess($resolvedPfxPath, 'Create package signing certificate PFX')) { $pfxDirectory = Split-Path -Parent $resolvedPfxPath if (-not [string]::IsNullOrWhiteSpace($pfxDirectory)) { $null = New-Item -ItemType Directory -Path $pfxDirectory -Force } $plainTextPassword = ConvertFrom-PackageSecureString -SecureString $Password try { [System.IO.File]::WriteAllBytes($resolvedPfxPath, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, $plainTextPassword)) } finally { $plainTextPassword = $null } $messages.Add("Created private signing certificate PFX '$resolvedPfxPath'.") | Out-Null if (-not [string]::IsNullOrWhiteSpace($resolvedCertificatePath)) { $certDirectory = Split-Path -Parent $resolvedCertificatePath if (-not [string]::IsNullOrWhiteSpace($certDirectory)) { $null = New-Item -ItemType Directory -Path $certDirectory -Force } if ([string]::Equals([System.IO.Path]::GetExtension($resolvedCertificatePath), '.pem', [System.StringComparison]::OrdinalIgnoreCase)) { ConvertTo-PackageCertificatePem -Certificate $certificate | Set-Content -LiteralPath $resolvedCertificatePath -Encoding ascii } else { [System.IO.File]::WriteAllBytes($resolvedCertificatePath, $certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)) } $messages.Add("Exported public signing certificate '$resolvedCertificatePath'.") | Out-Null } if (-not [string]::IsNullOrWhiteSpace($resolvedTrustExportPath)) { if ([string]::IsNullOrWhiteSpace($PublisherId)) { throw 'PublisherId is required when writing a package trust export.' } $entry = New-PackageTrustEntry -Certificate $certificate -PublisherId $PublisherId -PublisherName $PublisherName -SignerDisplayName $certificate.FriendlyName -TrustSource 'exported' Save-PackageJsonDocument -Path $resolvedTrustExportPath -Document (New-PackageTrustExportDocument -Entry $entry) $messages.Add("Wrote public package trust export '$resolvedTrustExportPath'.") | Out-Null } $profile = $null if ($usesFriendlyProfile) { $profile = Set-PackageSigningProfile -Name $Name -PublisherId $PublisherId -PublisherName $PublisherName -PfxPath $resolvedPfxPath -CertificatePath $resolvedCertificatePath -TrustExportPath $resolvedTrustExportPath -KeyThumbprint $thumbprint -CertificateSubject ([string]$certificate.Subject) -Password $Password $messages.Add("Saved local signing profile '$Name' for publisher '$PublisherId'.") | Out-Null $nextSteps.Add("Sign a package definition with: Sign-PackageDefinition -Path '<definition-json>'.") | Out-Null if (-not [string]::IsNullOrWhiteSpace($resolvedCertificatePath)) { $nextSteps.Add("Share the public .cer file with clients: $resolvedCertificatePath.") | Out-Null } if (-not [string]::IsNullOrWhiteSpace($resolvedTrustExportPath)) { $nextSteps.Add("Clients can import the .cer or trust JSON with Import-PackageTrust -Path '<public-trust-file>'.") | Out-Null } } } $result = [pscustomobject]@{ Name = if ($usesFriendlyProfile) { $Name } else { $null } PublisherId = $PublisherId PublisherName = $PublisherName PfxPath = $resolvedPfxPath CertificatePath = $resolvedCertificatePath TrustExportPath = $resolvedTrustExportPath Subject = [string]$certificate.Subject Thumbprint = $thumbprint NotBeforeUtc = $certificate.NotBefore.ToUniversalTime().ToString('o') NotAfterUtc = $certificate.NotAfter.ToUniversalTime().ToString('o') SignatureAlgorithm = [string]$certificate.SignatureAlgorithm.FriendlyName SigningProfilePath = if ($usesFriendlyProfile) { Get-PackageSigningProfileInventoryPath } else { $null } } return Add-PackageTrustCommandMessages -InputObject $result -Messages @($messages.ToArray()) -NextSteps @($nextSteps.ToArray()) } finally { if ($certificate) { $certificate.Dispose() } if ($rsa) { $rsa.Dispose() } } } function Sign-PackageDefinition { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string]$Path, [AllowNull()] [string]$CertificatePath = $null, [AllowNull()] [securestring]$Password = $null, [switch]$KeepSchemaVersion ) process { $definitionInfo = Read-PackageJsonDocument -Path $Path $publisherId = Get-PackageDefinitionPublisherIdForSigning -DefinitionInfo $definitionInfo $profile = $null $usesProfile = $false if ([string]::IsNullOrWhiteSpace($CertificatePath)) { $profile = Get-PackageSigningProfileByPublisherId -PublisherId $publisherId if (-not $profile) { $message = "No local package signing profile exists for publisher '$publisherId'." $nextStep = "Run: New-PackageSigningCertificate -Name '$publisherId' -Password (Read-Host -AsSecureString 'Catalog signing password')" Write-PackageExecutionMessage -Level 'ERR' -Message ("[FAIL] {0}" -f $message) Write-PackageExecutionMessage -Level 'WRN' -Message ("[NEXT] {0}" -f $nextStep) throw "$message $nextStep" } $CertificatePath = [string]$profile.pfxPath $Password = Unprotect-PackageSigningProfilePassword -ProtectedPassword ([string]$profile.protectedPassword) $usesProfile = $true } elseif (-not $Password) { $profile = Get-PackageSigningProfileByPfxPath -PfxPath $CertificatePath if ($profile) { $Password = Unprotect-PackageSigningProfilePassword -ProtectedPassword ([string]$profile.protectedPassword) $usesProfile = $true } else { throw "Password is required when signing with explicit certificate path '$CertificatePath'. Use -Password or create a local signing profile with New-PackageSigningCertificate -Name '$publisherId' -Password ..." } } $certificate = Import-PackageCertificate -Path $CertificatePath -Password $Password -WithPrivateKey try { if (-not $KeepSchemaVersion.IsPresent) { Set-PackageObjectProperty -InputObject $definitionInfo.Document -Name 'schemaVersion' -Value '1.7' if ($definitionInfo.Document.PSObject.Properties['$schema'] -and -not [string]::IsNullOrWhiteSpace([string]$definitionInfo.Document.'$schema')) { Set-PackageObjectProperty -InputObject $definitionInfo.Document -Name '$schema' -Value (([string]$definitionInfo.Document.'$schema') -replace '1\.6\.schema\.json', '1.7.schema.json') } } if ($PSCmdlet.ShouldProcess($definitionInfo.Path, 'Sign package definition')) { $signature = Invoke-PackageDefinitionDocumentSigning -Definition $definitionInfo.Document -Certificate $certificate Save-PackageJsonDocument -Path $definitionInfo.Path -Document $definitionInfo.Document $verification = Test-PackageDefinitionSignatureDocument -Definition $definitionInfo.Document -Certificate $certificate if ($usesProfile) { Set-PackageSigningProfileLastUsed -PublisherId $publisherId -KeyThumbprint $signature.KeyThumbprint } } else { $signature = $null $verification = $null } $messages = @( if ($signature) { "Signed package definition '$($definitionInfo.Path)' with key '$($signature.KeyThumbprint)'." "Verified embedded signature status '$($verification.Status)'." if ($usesProfile) { "Updated local signing profile last-used metadata for publisher '$publisherId'." } } else { "Prepared package definition signing for '$($definitionInfo.Path)' without writing changes." } ) $nextSteps = @( "Publish or keep the signed definition in the endpoint scanned by PackageEndpointInventory.json." "Make sure clients have imported the public .cer or trust JSON for publisher '$publisherId'." ) $result = [pscustomobject]@{ Path = $definitionInfo.Path PublisherId = $publisherId CertificatePath = [System.IO.Path]::GetFullPath($CertificatePath) UsedSigningProfile = [bool]$usesProfile KeyThumbprint = if ($signature) { $signature.KeyThumbprint } else { (($certificate.Thumbprint -replace '\s', '').ToUpperInvariant()) } CanonicalContentHash = if ($signature) { $signature.CanonicalContentHash } else { $null } VerificationStatus = if ($verification) { $verification.Status } else { 'WhatIf' } Valid = if ($verification) { [bool]$verification.Valid } else { $false } } return Add-PackageTrustCommandMessages -InputObject $result -Messages $messages -NextSteps $nextSteps } finally { $certificate.Dispose() } } } function Verify-PackageDefinitionSignature { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string]$Path, [AllowNull()] [string]$CertificatePath = $null, [AllowNull()] [string]$CertificatePem = $null, [switch]$RequireTrusted, [switch]$ErrorOnFailure ) process { $definitionInfo = Read-PackageJsonDocument -Path $Path $certificate = $null $trustInventory = $null try { if (-not [string]::IsNullOrWhiteSpace($CertificatePath)) { $certificate = Import-PackageCertificate -Path $CertificatePath } elseif (-not [string]::IsNullOrWhiteSpace($CertificatePem)) { $certificate = ConvertFrom-PackageCertificatePem -CertificatePem $CertificatePem } else { $trustInventory = (Get-PackageTrustInventoryInfo).Document } $result = Test-PackageDefinitionSignatureDocument -Definition $definitionInfo.Document -Certificate $certificate -TrustInventoryDocument $trustInventory $result | Add-Member -MemberType NoteProperty -Name 'Path' -Value $definitionInfo.Path -Force if ($RequireTrusted.IsPresent -and -not [bool]$result.Trusted) { $message = if ([string]::IsNullOrWhiteSpace([string]$result.ErrorMessage)) { "Package definition '$($definitionInfo.Path)' is not trusted." } else { [string]$result.ErrorMessage } if ($ErrorOnFailure.IsPresent) { throw $message } } elseif ($ErrorOnFailure.IsPresent -and -not [bool]$result.Valid) { $message = if ([string]::IsNullOrWhiteSpace([string]$result.ErrorMessage)) { "Package definition '$($definitionInfo.Path)' signature is not valid." } else { [string]$result.ErrorMessage } throw $message } return $result } finally { if ($certificate) { $certificate.Dispose() } } } } function Verify-PackageDefinitionCatalog { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [AllowNull()] [string]$CertificatePath = $null, [switch]$RequireTrusted, [switch]$ErrorOnFailure ) $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path $jsonFiles = if (Test-Path -LiteralPath $resolvedPath -PathType Leaf) { @((Get-Item -LiteralPath $resolvedPath)) } else { @(Get-ChildItem -LiteralPath $resolvedPath -Filter '*.json' -File -Recurse) } $results = @( foreach ($jsonFile in @($jsonFiles)) { Verify-PackageDefinitionSignature -Path $jsonFile.FullName -CertificatePath $CertificatePath -RequireTrusted:$RequireTrusted -ErrorOnFailure:$ErrorOnFailure } ) return [pscustomobject]@{ Path = $resolvedPath CheckedCount = $results.Count ValidCount = @($results | Where-Object { [bool]$_.Valid }).Count TrustedCount = @($results | Where-Object { [bool]$_.Trusted }).Count FailedCount = @($results | Where-Object { -not [bool]$_.Valid }).Count Results = @($results) } } function Remove-PackageDefinitionSignature { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('FullName')] [string]$Path, [switch]$KeepSchemaVersion ) process { $definitionInfo = Read-PackageJsonDocument -Path $Path if (-not $KeepSchemaVersion.IsPresent) { Set-PackageObjectProperty -InputObject $definitionInfo.Document -Name 'schemaVersion' -Value '1.7' if ($definitionInfo.Document.PSObject.Properties['$schema'] -and -not [string]::IsNullOrWhiteSpace([string]$definitionInfo.Document.'$schema')) { Set-PackageObjectProperty -InputObject $definitionInfo.Document -Name '$schema' -Value (([string]$definitionInfo.Document.'$schema') -replace '1\.6\.schema\.json', '1.7.schema.json') } } Set-PackageDefinitionUnsignedSignature -Definition $definitionInfo.Document if ($PSCmdlet.ShouldProcess($definitionInfo.Path, 'Remove package definition signature')) { Save-PackageJsonDocument -Path $definitionInfo.Path -Document $definitionInfo.Document } $result = [pscustomobject]@{ Path = $definitionInfo.Path SchemaVersion = [string]$definitionInfo.Document.schemaVersion Status = 'Unsigned' } return Add-PackageTrustCommandMessages -InputObject $result -Messages @("Removed embedded package definition signature from '$($definitionInfo.Path)'.") -NextSteps @("Re-sign this definition with Sign-PackageDefinition -Path '$($definitionInfo.Path)' before using strict catalog trust.") } } function Get-PackageTrust { [CmdletBinding()] param( [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$KeyThumbprint = $null ) $rows = @(Get-PackageTrustSummaries) if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { $rows = @($rows | Where-Object { [string]::Equals([string]$_.PublisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase) }) } if (-not [string]::IsNullOrWhiteSpace($KeyThumbprint)) { $normalizedThumbprint = (($KeyThumbprint -replace '\s', '').ToUpperInvariant()) $rows = @($rows | Where-Object { [string]::Equals(([string]$_.KeyThumbprint).ToUpperInvariant(), $normalizedThumbprint, [System.StringComparison]::OrdinalIgnoreCase) }) } return $rows } function Get-PackageSigningProfile { [CmdletBinding()] param( [AllowNull()] [string]$Name = $null, [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$KeyThumbprint = $null ) $rows = @(Get-PackageSigningProfileSummaries) if (-not [string]::IsNullOrWhiteSpace($Name)) { $rows = @($rows | Where-Object { [string]::Equals([string]$_.Name, $Name, [System.StringComparison]::OrdinalIgnoreCase) }) } if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { $rows = @($rows | Where-Object { [string]::Equals([string]$_.PublisherId, $PublisherId, [System.StringComparison]::OrdinalIgnoreCase) }) } if (-not [string]::IsNullOrWhiteSpace($KeyThumbprint)) { $normalizedThumbprint = (($KeyThumbprint -replace '\s', '').ToUpperInvariant()) $rows = @($rows | Where-Object { [string]::Equals(([string]$_.KeyThumbprint).ToUpperInvariant(), $normalizedThumbprint, [System.StringComparison]::OrdinalIgnoreCase) }) } return $rows } function Trust-PackageSigningCertificate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$CertificatePath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$PublisherId, [AllowNull()] [string]$PublisherName = $null, [AllowNull()] [string]$SignerDisplayName = $null, [string]$TrustSource = 'userApproved', [AllowNull()] [string]$TrustReason = $null, [switch]$Force ) $certificate = Import-PackageCertificate -Path $CertificatePath try { $entry = New-PackageTrustEntry -Certificate $certificate -PublisherId $PublisherId -PublisherName $PublisherName -SignerDisplayName $SignerDisplayName -TrustSource $TrustSource -TrustReason $TrustReason $documentInfo = Get-PackageTrustInventoryEditInfo $existing = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint ([string]$entry.keyThumbprint) if ($existing -and -not $Force.IsPresent) { throw "Package signing certificate '$($entry.keyThumbprint)' already exists in '$($documentInfo.Path)'. Use -Force to replace it." } if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Trust package signing certificate '$($entry.keyThumbprint)'")) { $documentInfo.Document.keys = @(@($documentInfo.Document.keys) | Where-Object { -not [string]::Equals(([string]$_.keyThumbprint).ToUpperInvariant(), ([string]$entry.keyThumbprint).ToUpperInvariant(), [System.StringComparison]::OrdinalIgnoreCase) }) + $entry Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $summary = Select-PackageTrustSummary -Entry $entry -InventoryPath $documentInfo.Path return Add-PackageTrustCommandMessages -InputObject $summary -Messages @("Trusted package signing certificate '$($entry.keyThumbprint)' for publisher '$PublisherId'.") -NextSteps @("Signed definitions for publisher '$PublisherId' can now resolve when catalogTrust.policy is strict.") } finally { $certificate.Dispose() } } function Untrust-PackageSigningCertificate { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$KeyThumbprint, [AllowNull()] [string]$Reason = 'Disabled by Untrust-PackageSigningCertificate.' ) $documentInfo = Get-PackageTrustInventoryEditInfo $entry = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint $KeyThumbprint if (-not $entry) { throw "Package signing certificate '$KeyThumbprint' was not found in '$($documentInfo.Path)'." } if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Untrust package signing certificate '$KeyThumbprint'")) { Set-PackageObjectProperty -InputObject $entry -Name 'enabled' -Value $false Set-PackageObjectProperty -InputObject $entry -Name 'trustReason' -Value $Reason Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $summary = Select-PackageTrustSummary -Entry $entry -InventoryPath $documentInfo.Path return Add-PackageTrustCommandMessages -InputObject $summary -Messages @("Disabled package signing trust '$KeyThumbprint'.") -NextSteps @("Use Get-PackageTrust to review current trust status, or Import-PackageTrust to add a replacement key.") } function Revoke-PackageSigningCertificate { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$KeyThumbprint, [AllowNull()] [string]$Reason = 'Revoked by Revoke-PackageSigningCertificate.' ) $documentInfo = Get-PackageTrustInventoryEditInfo $entry = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint $KeyThumbprint if (-not $entry) { throw "Package signing certificate '$KeyThumbprint' was not found in '$($documentInfo.Path)'. Use Block-PackageSigningCertificate to block an unknown thumbprint." } $normalizedThumbprint = (([string]$entry.keyThumbprint -replace '\s', '').ToUpperInvariant()) if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Revoke package signing certificate '$normalizedThumbprint'")) { Set-PackageObjectProperty -InputObject $entry -Name 'enabled' -Value $false Set-PackageObjectProperty -InputObject $entry -Name 'revokedAtUtc' -Value ([DateTime]::UtcNow.ToString('o')) Set-PackageObjectProperty -InputObject $entry -Name 'revocationReason' -Value $Reason Set-PackageObjectProperty -InputObject $entry -Name 'revokedBy' -Value ([Environment]::UserName) if (-not (Test-PackageKeyThumbprintRevoked -TrustInventoryDocument $documentInfo.Document -KeyThumbprint $normalizedThumbprint -PublisherId ([string]$entry.publisherId))) { $documentInfo.Document.revokedKeys = @($documentInfo.Document.revokedKeys) + [pscustomobject][ordered]@{ keyThumbprint = $normalizedThumbprint publisherId = [string]$entry.publisherId source = 'user' reason = $Reason addedAtUtc = [DateTime]::UtcNow.ToString('o') } } Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $summary = Select-PackageTrustSummary -Entry $entry -InventoryPath $documentInfo.Path return Add-PackageTrustCommandMessages -InputObject $summary -Messages @("Revoked package signing certificate '$normalizedThumbprint'.") -NextSteps @("Replace affected signed definitions with a new trusted signing key.") } function Block-PackageSigningCertificate { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$KeyThumbprint, [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$Reason = 'Blocked by Block-PackageSigningCertificate.' ) $documentInfo = Get-PackageTrustInventoryEditInfo $normalizedThumbprint = (($KeyThumbprint -replace '\s', '').ToUpperInvariant()) if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { Assert-PackagePublisherId -PublisherId $PublisherId } if (Test-PackageKeyThumbprintRevoked -TrustInventoryDocument $documentInfo.Document -KeyThumbprint $normalizedThumbprint -PublisherId $PublisherId) { throw "Package signing certificate '$normalizedThumbprint' is already blocked in '$($documentInfo.Path)'." } $block = [ordered]@{ keyThumbprint = $normalizedThumbprint source = 'user' reason = $Reason addedAtUtc = [DateTime]::UtcNow.ToString('o') } if (-not [string]::IsNullOrWhiteSpace($PublisherId)) { $block['publisherId'] = $PublisherId } if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Block package signing certificate '$normalizedThumbprint'")) { $documentInfo.Document.revokedKeys = @($documentInfo.Document.revokedKeys) + ([pscustomobject]$block) Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $result = [pscustomobject]$block return Add-PackageTrustCommandMessages -InputObject $result -Messages @("Blocked package signing certificate '$normalizedThumbprint'.") -NextSteps @("Use Import-PackageTrust or Trust-PackageSigningCertificate to trust a replacement key when needed.") } function Remove-PackageTrust { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true)] [string]$KeyThumbprint ) $documentInfo = Get-PackageTrustInventoryEditInfo $entry = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint $KeyThumbprint if (-not $entry) { throw "Package signing certificate '$KeyThumbprint' was not found in '$($documentInfo.Path)'." } $normalizedThumbprint = (([string]$entry.keyThumbprint -replace '\s', '').ToUpperInvariant()) if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Remove package trust '$normalizedThumbprint'")) { $documentInfo.Document.keys = @($documentInfo.Document.keys | Where-Object { -not [string]::Equals((([string]$_.keyThumbprint -replace '\s', '').ToUpperInvariant()), $normalizedThumbprint, [System.StringComparison]::OrdinalIgnoreCase) }) Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $result = [pscustomobject]@{ Action = 'Remove' KeyThumbprint = $normalizedThumbprint InventoryPath = $documentInfo.Path Status = 'Removed' } return Add-PackageTrustCommandMessages -InputObject $result -Messages @("Removed package trust '$normalizedThumbprint' from '$($documentInfo.Path)'.") -NextSteps @("Use Import-PackageTrust to add this public key again if definitions still need it.") } function Export-PackageTrust { [CmdletBinding()] param( [AllowNull()] [string]$KeyThumbprint = $null, [AllowNull()] [string]$CertificatePath = $null, [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$PublisherName = $null, [AllowNull()] [string]$SignerDisplayName = $null, [string]$TrustSource = 'exported', [AllowNull()] [string]$TrustReason = $null, [AllowNull()] [string]$OutputPath = $null ) if ([string]::IsNullOrWhiteSpace($KeyThumbprint) -and [string]::IsNullOrWhiteSpace($CertificatePath)) { throw 'Use either -KeyThumbprint or -CertificatePath.' } if (-not [string]::IsNullOrWhiteSpace($CertificatePath)) { if ([string]::IsNullOrWhiteSpace($PublisherId)) { throw 'PublisherId is required when exporting trust directly from a certificate file.' } $certificate = Import-PackageCertificate -Path $CertificatePath try { $entry = New-PackageTrustEntry -Certificate $certificate -PublisherId $PublisherId -PublisherName $PublisherName -SignerDisplayName $SignerDisplayName -TrustSource $TrustSource -TrustReason $TrustReason } finally { $certificate.Dispose() } } else { $documentInfo = Get-PackageTrustInventoryInfo $entry = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint $KeyThumbprint if (-not $entry) { throw "Package trust '$KeyThumbprint' was not found in '$($documentInfo.Path)'." } } $export = New-PackageTrustExportDocument -Entry $entry if (-not [string]::IsNullOrWhiteSpace($OutputPath)) { Save-PackageJsonDocument -Path $OutputPath -Document $export $messages = @("Wrote package trust export '$([System.IO.Path]::GetFullPath($OutputPath))'.") $nextSteps = @("Clients can import this public trust export with Import-PackageTrust -Path '$([System.IO.Path]::GetFullPath($OutputPath))'.") $export | Add-Member -MemberType NoteProperty -Name 'OutputPath' -Value ([System.IO.Path]::GetFullPath($OutputPath)) -Force return Add-PackageTrustCommandMessages -InputObject $export -Messages $messages -NextSteps $nextSteps } return $export } function Import-PackageTrust { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [string]$Path, [AllowNull()] [string]$PublisherId = $null, [AllowNull()] [string]$PublisherName = $null, [AllowNull()] [string]$SignerDisplayName = $null, [string]$TrustSource = 'imported', [AllowNull()] [string]$TrustReason = $null, [switch]$Force ) $resolvedPath = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path $extension = [System.IO.Path]::GetExtension($resolvedPath) $importLabel = $resolvedPath $certificate = $null $importInfo = $null if ($extension -in @('.cer', '.crt', '.pem')) { $certificate = Import-PackageCertificate -Path $resolvedPath try { if ([string]::IsNullOrWhiteSpace($PublisherId)) { $PublisherId = Resolve-PackagePublisherIdFromCertificate -Certificate $certificate } if ([string]::IsNullOrWhiteSpace($PublisherId)) { throw "Package trust certificate '$resolvedPath' does not contain an inferable publisher id. Use Import-PackageTrust -Path '$resolvedPath' -PublisherId '<publisherId>'." } if ([string]::IsNullOrWhiteSpace($PublisherName)) { $PublisherName = $PublisherId } $entries = @(New-PackageTrustEntry -Certificate $certificate -PublisherId $PublisherId -PublisherName $PublisherName -SignerDisplayName $SignerDisplayName -TrustSource $TrustSource -TrustReason $TrustReason) } finally { $certificate.Dispose() } } else { $importInfo = Read-PackageJsonDocument -Path $resolvedPath $entries = if ($importInfo.Document.PSObject.Properties['keys']) { @($importInfo.Document.keys) } else { @($importInfo.Document) } } if ($entries.Count -eq 0) { throw "Package trust import '$importLabel' does not contain any keys." } $documentInfo = Get-PackageTrustInventoryEditInfo $imported = New-Object System.Collections.Generic.List[object] foreach ($entry in @($entries)) { foreach ($requiredProperty in @('publisherId', 'publisherName', 'keyThumbprint', 'certificatePem')) { if (-not $entry.PSObject.Properties[$requiredProperty] -or [string]::IsNullOrWhiteSpace([string]$entry.$requiredProperty)) { throw "Package trust import '$importLabel' has an entry missing '$requiredProperty'." } } $normalizedThumbprint = (([string]$entry.keyThumbprint -replace '\s', '').ToUpperInvariant()) $existing = Get-PackageTrustEntryByThumbprint -Document $documentInfo.Document -KeyThumbprint $normalizedThumbprint if ($existing -and -not $Force.IsPresent) { throw "Package signing certificate '$normalizedThumbprint' already exists in '$($documentInfo.Path)'. Use -Force to replace it." } Set-PackageObjectProperty -InputObject $entry -Name 'keyThumbprint' -Value $normalizedThumbprint if (-not $entry.PSObject.Properties['trustSource']) { Set-PackageObjectProperty -InputObject $entry -Name 'trustSource' -Value 'imported' } if (-not $entry.PSObject.Properties['trustedAtUtc']) { Set-PackageObjectProperty -InputObject $entry -Name 'trustedAtUtc' -Value ([DateTime]::UtcNow.ToString('o')) } if (-not $entry.PSObject.Properties['enabled']) { Set-PackageObjectProperty -InputObject $entry -Name 'enabled' -Value $true } if (-not $entry.PSObject.Properties['trustedBy']) { Set-PackageObjectProperty -InputObject $entry -Name 'trustedBy' -Value ([Environment]::UserName) } $documentInfo.Document.keys = @(@($documentInfo.Document.keys) | Where-Object { -not [string]::Equals((([string]$_.keyThumbprint -replace '\s', '').ToUpperInvariant()), $normalizedThumbprint, [System.StringComparison]::OrdinalIgnoreCase) }) + $entry $imported.Add($entry) | Out-Null } if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Import package trust from '$importLabel'")) { Save-PackageTrustInventoryDocument -DocumentInfo $documentInfo } $messages = @( "Imported $($imported.Count) package signing trust entr$(if ($imported.Count -eq 1) { 'y' } else { 'ies' }) from '$importLabel'." "Updated package trust inventory '$($documentInfo.Path)'." ) $nextSteps = @( "Run Invoke-Package after the matching endpoint is registered." "Use Get-PackageTrust to review trusted package signing keys." ) $result = [pscustomobject]@{ Action = 'Import' InventoryPath = $documentInfo.Path SourcePath = $importLabel ImportedCount = $imported.Count Keys = @($imported.ToArray() | ForEach-Object { Select-PackageTrustSummary -Entry $_ -InventoryPath $documentInfo.Path }) } return Add-PackageTrustCommandMessages -InputObject $result -Messages $messages -NextSteps $nextSteps } |