Runtime/OSDStageCertificate.ps1
|
function Read-OSDStageCertificate { [CmdletBinding()] param( [Parameter(Mandatory)][string]$LiteralPath, [Parameter(Mandatory)][ValidateSet('Root','CA')][string]$Store ) $file=Get-Item -LiteralPath $LiteralPath -ErrorAction Stop if ($file.PSIsContainer -or $file.Extension -notin @('.cer','.crt') -or $file.Length -gt 1MB) { throw 'Provide an individual public .cer or .crt certificate of at most 1 MB.' } $bytes=[IO.File]::ReadAllBytes($file.FullName) # Check the container before loading: never import keys from a disguised PFX. if ([Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($bytes) -ne [Security.Cryptography.X509Certificates.X509ContentType]::Cert) { throw 'Only individual public X.509 certificates are supported; PFX and bundles are not accepted.' } $text=[Text.Encoding]::ASCII.GetString($bytes) if ($text.Contains('-----BEGIN')) { if ($text -notmatch '\A\s*-----BEGIN CERTIFICATE-----\s*[A-Za-z0-9+/=\s]+-----END CERTIFICATE-----\s*\z') { throw 'PEM input must contain exactly one certificate and no private key or other content.' } } $cert=[Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) try { if (-not $text.Contains('-----BEGIN') -and [Convert]::ToBase64String($bytes) -ne [Convert]::ToBase64String($cert.RawData)) { throw 'DER input must contain exactly one certificate without trailing content.' } if ($cert.HasPrivateKey) { throw 'Private keys are not supported.' } $constraints=@($cert.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.19' }) if ($constraints.Count -ne 1 -or -not $constraints[0].CertificateAuthority) { throw 'Root and CA stores require a certificate with the CA basic constraint.' } if ($Store -eq 'Root' -and [Convert]::ToBase64String($cert.SubjectName.RawData) -ne [Convert]::ToBase64String($cert.IssuerName.RawData)) { throw 'Root requires a self-issued CA certificate. Use CA for intermediate certificates.' } [pscustomobject]@{ Subject=$cert.Subject;Thumbprint=$cert.Thumbprint;Store=$Store Certificate=[Convert]::ToBase64String($cert.RawData) } } finally { $cert.Dispose() } } function ConvertFrom-OSDStageCertificateOperation { param([Parameter(Mandatory)]$Operation) if ([string]$Operation.Store -notin @('Root','CA')) { throw 'Unsupported certificate store.' } if ([string]$Operation.Thumbprint -notmatch '^[A-Fa-f0-9]{40}$') { throw 'Invalid certificate thumbprint.' } $bytes=[Convert]::FromBase64String([string]$Operation.Certificate) if ($bytes.Length -gt 1MB -or [Security.Cryptography.X509Certificates.X509Certificate2]::GetCertContentType($bytes) -ne [Security.Cryptography.X509Certificates.X509ContentType]::Cert) { throw 'Invalid public certificate payload.' } $cert=[Security.Cryptography.X509Certificates.X509Certificate2]::new($bytes) try { if ($cert.HasPrivateKey -or $cert.Thumbprint -ne [string]$Operation.Thumbprint) { throw 'Certificate identity mismatch.' } $constraints=@($cert.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.19' }) if ($constraints.Count -ne 1 -or -not $constraints[0].CertificateAuthority) { throw 'Certificate is not a CA.' } if ([string]$Operation.Store -eq 'Root' -and [Convert]::ToBase64String($cert.SubjectName.RawData) -ne [Convert]::ToBase64String($cert.IssuerName.RawData)) { throw 'Root requires a self-issued CA.' } return $cert } catch { $cert.Dispose();throw } } function Install-OSDStagePublicCertificate { param([Parameter(Mandatory)]$Certificate,[Parameter(Mandatory)][ValidateSet('Root','CA')][string]$Store) $target=[Security.Cryptography.X509Certificates.X509Store]::new($Store,[Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) try { $target.Open([Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) $existing=$target.Certificates.Find([Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint,$Certificate.Thumbprint,$false) if ($existing.Count -gt 0) { return 'AlreadyPresent' } $target.Add($Certificate) return 'Imported' } finally { $target.Close();$target.Dispose() } } function Invoke-OSDStageCertificateOperation { param([Parameter(Mandatory)]$Operation) $cert=ConvertFrom-OSDStageCertificateOperation -Operation $Operation try { $outcome=Install-OSDStagePublicCertificate -Certificate $cert -Store ([string]$Operation.Store) [pscustomobject]@{ Subject=$cert.Subject;Thumbprint=$cert.Thumbprint;Store=('LocalMachine\'+[string]$Operation.Store) Status='Succeeded';Outcome=$outcome;Error=$null } } finally { $cert.Dispose() } } function Get-OSDStageCertificatePackageOperations { [CmdletBinding()] param( [Parameter(Mandatory)][string]$PayloadRoot, [Parameter(Mandatory)][ValidateSet('Root','CA')][string]$Store ) if (-not (Test-Path -LiteralPath $PayloadRoot -PathType Container)) { throw 'Certificate package directory is missing.' } $entries=@(Get-ChildItem -LiteralPath $PayloadRoot -Recurse -Force -ErrorAction Stop) if (@($entries | Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint }).Count) { throw 'Certificate packages cannot contain reparse points.' } $files=@($entries | Where-Object { -not $_.PSIsContainer } | Sort-Object FullName) if ($files.Count -eq 0) { throw 'Certificate packages require at least one public .cer or .crt file.' } if (@($files | Where-Object { $_.Extension -notin @('.cer','.crt') }).Count) { throw 'Certificate packages may contain only public .cer or .crt files; scripts, PFX and support files are not allowed.' } $seen=@{} foreach($file in $files) { $operation=Read-OSDStageCertificate -LiteralPath $file.FullName -Store $Store if (-not $seen.ContainsKey($operation.Thumbprint)) { $seen[$operation.Thumbprint]=$true $operation } } } function Invoke-OSDStageCertificatePackage { [CmdletBinding()] param( [Parameter(Mandatory)][string]$PayloadRoot, [Parameter(Mandatory)][string]$PackageId, [Parameter(Mandatory)][ValidateSet('Root','CA')][string]$Store, [Parameter(Mandatory)][string]$DeploymentId, [Parameter(Mandatory)][string]$ResultPath ) $items=@() $failure=$null try { # Materialize and validate all files before the first trust-store write. $operations=@(Get-OSDStageCertificatePackageOperations -PayloadRoot $PayloadRoot -Store $Store) $items=@(foreach($operation in $operations) { try { Invoke-OSDStageCertificateOperation -Operation $operation } catch { [pscustomobject]@{ Subject=$operation.Subject;Thumbprint=$operation.Thumbprint;Store=('LocalMachine\'+$Store) Status='Failed';Outcome='Failed';Error=$_.Exception.Message } } }) } catch { $failure=$_.Exception.Message } $failed=@($items | Where-Object Status -eq Failed).Count $exitCode=if($failure -or $failed){1}else{0} $record=[ordered]@{ SchemaVersion=2;DeploymentId=$DeploymentId;PackageId=$PackageId Status=if($exitCode){'Failed'}else{'Succeeded'} OperationCount=$items.Count;SuccessfulOperationCount=($items.Count-$failed) FailedOperationCount=$failed;RestartNeeded=$false;Items=$items Error=if($failure){$failure}elseif($failed){"$failed certificate operation(s) failed."}else{$null} } New-Item -ItemType Directory -Path (Split-Path $ResultPath -Parent) -Force -ErrorAction Stop | Out-Null $record | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $ResultPath -Encoding UTF8 -ErrorAction Stop [pscustomobject]@{ExitCode=$exitCode;Integrity='Sha256Validated';ResultPath=$ResultPath} } |