Public/Set-VellumPdfSignature.ps1

function Set-VellumPdfSignature {
    <#
    .SYNOPSIS
        Stages a PAdES digital signature to be applied when the document is saved.
    .DESCRIPTION
        Builds a VellumPdf.Signing.PdfSignatureSettings from the supplied
        certificate and metadata and stages it on the document. The signature is
        applied by Save-VellumPdfDocument, which signs the document while writing
        the file (VellumPdf signs at serialization time via
        SigningExtensions.Sign; a signature cannot be added to an already-written
        document through this module).

        The signature is a PAdES baseline signature (SubFilter
        ETSI.CAdES.detached). PDF/A conformance and signing compose: a PDF/A-2b
        document can be signed. Encryption and signing cannot be combined - the
        library rejects the combination at save time, and this cmdlet (and
        Protect-VellumPdfDocument) fail fast with a clear error instead.

        Calling Set-VellumPdfSignature again before saving replaces the staged
        signature settings, consistent with Set-* semantics.

        CERTIFICATE: any [X509Certificate2] with a private key works - from
        Get-PfxCertificate, the cert: drive (Cert:\CurrentUser\My\<thumbprint>),
        or X509CertificateLoader/X509Certificate2 .NET APIs. Long-term validation
        (LTV: embedded OCSP/CRL) is not yet provided by the library and is out of
        scope here.
    .PARAMETER Document
        The live VellumPdf document flowing through the pipeline. The same
        instance is returned after the signature settings are staged, enabling
        chaining.
    .PARAMETER Certificate
        The signing certificate. Must include a private key (HasPrivateKey).
        Typical sources: Get-PfxCertificate -FilePath ./signer.pfx, or
        Get-Item Cert:\CurrentUser\My\<thumbprint>.
    .PARAMETER Reason
        Optional reason for signing, recorded in the signature dictionary
        (/Reason) and shown by PDF viewers (e.g. 'Approved', 'I am the author').
    .PARAMETER Location
        Optional physical or logical location of signing, recorded as /Location.
    .PARAMETER ContactInfo
        Optional contact information for the signer (e.g. an email address),
        recorded as /ContactInfo.
    .PARAMETER SignerName
        Optional display name of the signer, recorded as /Name. When omitted,
        viewers typically fall back to the certificate subject.
    .PARAMETER SigningTime
        Optional claimed signing time recorded in the signature. When omitted the
        library uses the current time at save.
    .EXAMPLE
        $cert = Get-PfxCertificate -FilePath ./signer.pfx
        New-VellumPdfDocument |
            Add-VellumPdfParagraph -Text 'Signed content.' |
            Set-VellumPdfSignature -Certificate $cert -Reason 'Approved' |
            Save-VellumPdfDocument -Path ./signed.pdf
    .EXAMPLE
        # Sign a PDF/A-2b archival document with a certificate from the store
        $cert = Get-Item Cert:\CurrentUser\My\1234567890ABCDEF1234567890ABCDEF12345678
        New-VellumPdfDocument -Conformance PdfA2b |
            Set-VellumPdfDocumentInfo -Title 'Contract' -Author 'Acme' |
            Add-VellumPdfParagraph -Text 'Terms.' -FontHandle $font |
            Set-VellumPdfSignature -Certificate $cert -Location 'Amsterdam' `
                -ContactInfo 'legal@acme.example' |
            Save-VellumPdfDocument -Path ./contract.pdf
    .OUTPUTS
        VellumPdf.Layout.Document (the same instance, for chaining)
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Mutates an in-memory document object only; no external/system state change.')]
    [CmdletBinding()]
    [OutputType([VellumPdf.Layout.Document])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [VellumPdf.Layout.Document]$Document,

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

        [string]$Reason,

        [string]$Location,

        [string]$ContactInfo,

        [string]$SignerName,

        [System.Nullable[System.DateTimeOffset]]$SigningTime
    )

    process {
        Assert-VellumPdfDocumentOpen -Document $Document -CommandName 'Set-VellumPdfSignature'

        # The library rejects Encrypt() + Sign() at save time; fail fast here
        # with the same constraint so the error points at the right cmdlet.
        if ($Document.PSObject.Properties['PSVellumProtected']) {
            throw ('Set-VellumPdfSignature: encryption and digital signatures cannot be combined. ' +
                'Remove the Protect-VellumPdfDocument call to sign this document.')
        }

        if (-not $Certificate.HasPrivateKey) {
            throw ('Set-VellumPdfSignature: the certificate does not include a private key. ' +
                'Signing requires the private key; load the certificate from a PFX ' +
                '(Get-PfxCertificate) or a store entry that has the key.')
        }

        $settings = [VellumPdf.Signing.PdfSignatureSettings]::new()
        $settings.Certificate = $Certificate
        if ($PSBoundParameters.ContainsKey('Reason'))      { $settings.Reason      = $Reason }
        if ($PSBoundParameters.ContainsKey('Location'))    { $settings.Location    = $Location }
        if ($PSBoundParameters.ContainsKey('ContactInfo')) { $settings.ContactInfo = $ContactInfo }
        if ($PSBoundParameters.ContainsKey('SignerName'))  { $settings.SignerName  = $SignerName }
        if ($PSBoundParameters.ContainsKey('SigningTime')) { $settings.SigningTime = $SigningTime }

        # Stage for Save-VellumPdfDocument; Set-* semantics allow replacing a
        # previously staged signature.
        $existing = $Document.PSObject.Properties['PSVellumSignature']
        if ($existing) {
            $existing.Value = $settings
        }
        else {
            $Document.PSObject.Properties.Add([psnoteproperty]::new('PSVellumSignature', $settings))
        }
        $Document
    }
}