Electronic Signature Manifestation under 21 CFR Part 11

An electronic signature on a regulatory submission has to do two jobs at once. It has to be readable by the human reviewer who opens the cover letter — a printed name, a date, a statement of what the signer was attesting to. And it has to be verifiable by a machine years later, in a way that survives a hostile question from an inspector: could this signature have been copied onto a different document? Most teams get the first job right and the second wrong, because a visible stamp that merely looks signed satisfies a reviewer but not an auditor. This area of the Electronic Submission Gateway & E-Signature Automation domain is where a signature stops being a picture and becomes a cryptographically bound assertion that meets 21 CFR Part 11 Subpart C.

The two areas this work sits beside are the structural half of the pipeline — eCTD Sequence Assembly & Lifecycle Management, which decides what goes into a sequence — and the transport half that ships it. Signing happens after assembly and validation and before transport, because a signature binds to the exact bytes that will be sent; sign too early and a downstream edit silently invalidates it without anyone noticing until the gateway, or worse, an inspector, notices for you.

Regulatory affairs teams often inherit this problem from a paper-based process that never fully translated: a printed cover letter, a wet signature, a scanned PDF. That workflow satisfied nobody’s threat model except “does the page have ink on it.” Moving to a Python-automated signing pipeline is not simply digitizing the same weak guarantee faster — it is an opportunity to make the guarantee actually hold, by tying the signature mathematically to the bytes it covers instead of trusting that nobody swapped the page after the pen came off it. That shift in what “signed” means is the organizing idea behind everything that follows.

Decision flow

The manifestation pipeline takes a validated artifact and a signer’s authenticated identity as inputs and produces a signed artifact plus an audit event as outputs. Nothing in the middle is optional: skipping the identity check produces a signature with no accountable signer, and skipping the hash binding produces a signature an inspector can prove is severable from its record.

Electronic signature manifestation pipeline A signer identity and a validated record feed identity verification, then record hashing, manifestation assembly, cryptographic signing, and PDF stamping. The pipeline produces a signed artifact and a signature record as outputs. Each of the five stages writes to a write-once, hash-chained audit log that satisfies 21 CFR Part 11. INPUT SIGNING PIPELINE OUTPUTS Signer + record Identity §11.200 Hash SHA-256 Manifest §11.50 Sign §11.70 Stamp PDF Signed artifact Signature record Append-only audit log write-once · hash-chained · attributable · 21 CFR Part 11

Reading the flow left to right: an identity stage confirms the signer presented the two distinct identification components §11.200 requires, a hashing stage fixes the exact bytes being attested to, a manifestation stage assembles the human-readable printed name, timestamp, and meaning §11.50 requires, a signing stage produces a cryptographic signature over the hash so the signature is bound to the record under §11.70, and a stamping stage writes the manifestation into the submission PDF for a reviewer to read without any tooling.

The order of hashing and manifestation assembly is not interchangeable. Hashing has to run against the artifact as it exists before any visible signature text is added, because the whole point of the exercise is to bind the signature to a fixed, unambiguous set of bytes. If the manifestation block were drawn onto the page first and the file hashed second, the signature would cover a document that already displays the very signature it is attesting to — a circular dependency that also means any correction to the printed name or meaning, however small, forces the hash and the signature to be recomputed together with no clean way to tell which changed first.

Library and tooling landscape

Part 11 does not mandate a cryptographic algorithm or a file format; it mandates properties (linkage, non-repudiation, a specific set of visible fields). That leaves real engineering choices, and some options are worse than they look.

Option Role Clinical-grade verdict
cryptography Hashing, RSA/ECDSA signing, key handling Recommended — actively maintained, audited primitives, no home-grown crypto
pypdf Read/write PDF structure, embed manifestation metadata Recommended — maintained fork that replaced PyPDF2
reportlab Render the human-readable manifestation block onto a page Recommended for stamping a visible signature block
A cloud KMS (AWS KMS, Azure Key Vault, GCP KMS) Hold the signing private key; sign without exporting it Recommended for production — the key never enters application memory
PyPDF2 (legacy) PDF reading/writing Deprecated — unmaintained since 2022; import pypdf instead
Hand-rolled signature via string concatenation Ad hoc “signature” text with no cryptographic binding Non-compliant — satisfies no part of §11.70; do not use for a real submission

Deprecated dependency: PyPDF2 stopped receiving updates and its maintainers now point users to pypdf, which is the same lineage under active maintenance. A submission pipeline that still imports PyPDF2 should be treated as a finding waiting to happen — pin pypdf in pyproject.toml and remove the old import entirely, not just alongside it.

The one design decision worth calling out explicitly: never store the signing private key in application configuration or environment variables in a production deployment. Environment variables are the right place for a KMS key identifier and region, not for key material itself — the same boundary discipline covered in Security Boundaries for Clinical Data.

The choice of algorithm is a secondary decision but still worth making deliberately rather than by default. RSA-2048 or stronger with PSS padding and ECDSA over P-256 both satisfy the non-repudiation property Part 11 needs; ECDSA produces a smaller signature and a faster signing operation, which matters when a single sequence carries dozens of leaf-level attestations, while RSA remains the more universally recognized choice for reviewers and validators outside the pipeline itself. Whichever is chosen, pin it at the KMS key level rather than leaving it to a per-call parameter — a submission’s signatures should all be verifiable the same way years later, and a mixed-algorithm history is one more thing an inspector has to be walked through instead of one they can verify themselves.

Step-by-step implementation

The manifestation pipeline is five small, independently testable steps. Each one raises rather than guesses when its input is incomplete.

1. Model the signature’s regulatory components

§11.50 requires three things to appear with every signed record: the signer’s printed name, the date and time of signing, and the meaning of the signature. §11.200 separately requires the signature itself to be built from at least two distinct identification components (for example, a unique identifier plus a password, or a unique identifier plus a biometric) when it is not a biometric signature on its own. Modeling both requirements as data — rather than as prose in a design document — is what lets validation enforce them mechanically.

"""Typed model of a Part 11 electronic signature's regulatory components."""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum


class SignatureMeaning(str, Enum):
    """The attestation a signature carries, per 21 CFR 11.50(a)."""

    AUTHORSHIP = "authorship"
    REVIEW = "review"
    APPROVAL = "approval"
    RESPONSIBILITY = "responsibility"


@dataclass(frozen=True)
class SignerIdentity:
    """Two distinct identification components, per 21 CFR 11.200(a)(1)."""

    unique_id: str
    credential_verified: bool  # e.g. password or biometric check already passed

    def __post_init__(self) -> None:
        if not self.unique_id:
            raise ValueError("signer must present a unique identification component")
        if not self.credential_verified:
            raise ValueError("signer must present a second, verified identification component")


@dataclass(frozen=True)
class SignatureManifestation:
    """The human-readable content required to accompany a signed record."""

    signer_name: str
    signer_identity: SignerIdentity
    meaning: SignatureMeaning
    signed_at: datetime

    @classmethod
    def now(cls, signer_name: str, signer_identity: SignerIdentity, meaning: SignatureMeaning) -> "SignatureManifestation":
        return cls(signer_name, signer_identity, meaning, datetime.now(timezone.utc))

Fixing signed_at to UTC removes an entire class of ambiguity — a reviewer in one time zone and an inspector in another should never have to reconcile what “the date of signing” meant.

2. Hash the record before anything else touches it

The record hash is what the signature will ultimately cover, so it has to be computed over the exact bytes that will be transmitted — before stamping, not after. Hashing after stamping would bind the signature to a document that includes the signature’s own visible text, which is circular and which most verifiers reject outright.

"""Compute the provenance hash a signature will be bound to."""
from __future__ import annotations

import hashlib
from pathlib import Path


def record_sha256(record_path: Path) -> str:
    """SHA-256 of the exact bytes being signed; §11.70 binds the signature to this value."""
    digest = hashlib.sha256()
    with record_path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(65536), b""):
            digest.update(chunk)
    return digest.hexdigest()

3. Sign the hash, never the file

Signing the hash rather than the raw file keeps the signing operation small, fast, and — critically — reusable with a KMS API that accepts a digest rather than an arbitrary payload. The private key never leaves the KMS boundary; the application only ever sees a signature blob.

"""Produce a cryptographic signature over a record's hash using a KMS-held key."""
from __future__ import annotations

import os
from dataclasses import dataclass


class SigningError(Exception):
    """Raised when the signing backend cannot produce a signature."""


@dataclass(frozen=True)
class SignatureRecord:
    """§11.70: a signature bound to the record hash it covers, never the raw file."""

    record_sha256: str
    signature_b64: str
    key_id: str
    signer_name: str
    meaning: str
    signed_at: str  # ISO 8601 UTC


def sign_record_hash(record_sha256: str, manifestation: "SignatureManifestation", kms_client) -> SignatureRecord:
    key_id = os.environ.get("SIGNING_KEY_ID")
    if not key_id:
        raise RuntimeError("Missing required environment variable: SIGNING_KEY_ID")
    try:
        response = kms_client.sign(key_id=key_id, message_digest=record_sha256, digest_algorithm="SHA256")
    except Exception as exc:  # backend-specific exceptions vary by KMS
        raise SigningError(f"KMS signing failed for key {key_id!r}: {exc}") from exc
    return SignatureRecord(
        record_sha256=record_sha256,
        signature_b64=response.signature_b64,
        key_id=key_id,
        signer_name=manifestation.signer_name,
        meaning=manifestation.meaning.value,
        signed_at=manifestation.signed_at.isoformat(),
    )

4. Stamp the manifestation for human review

The cryptographic signature satisfies the machine-verifiable half of the requirement; a visible block satisfies the reviewer who opens the PDF without tooling. Both are required — a Part 11 signature that is only a database row with no human-readable manifestation fails the spirit of §11.50 even if the cryptography is sound. The concrete PDF-stamping code, including how the visible block references the hash it is bound to, lives in Compliant Electronic Signature Manifestations in Submission PDFs.

5. Emit the signature audit event

The signing operation is not complete until it is recorded. The audit event is the durable link between a person, a meaning, a hash, and a timestamp — the four facts an inspector will ask for together.

"""Emit an audit event for a completed signature operation."""
from __future__ import annotations

import json
import logging

logger = logging.getLogger("part11.signature")


def record_signature(signature: "SignatureRecord") -> None:
    event = {
        "action": "record_signed",
        "record_sha256": signature.record_sha256,
        "signer_name": signature.signer_name,
        "meaning": signature.meaning,
        "signed_at": signature.signed_at,
        "key_id": signature.key_id,
    }
    logger.info(json.dumps(event, sort_keys=True))

Validation and audit-trail integration

A signature is only as trustworthy as the checks that ran before and after it. Before signing, confirm the artifact has already cleared structural checks — a signature applied to a structurally invalid sequence still has to be redone once technical validation surfaces a defect, wasting a signer’s attestation. After signing, verification is a pure function of three inputs: the record’s current bytes, the stored signature, and the signer’s public key — recompute the hash, and if it no longer matches what the signature covers, the record was altered after signing and the signature is void by construction, exactly as §11.70 intends.

Every signing event lands in the same append-only audit log used across the platform, so reconstructing who signed a given submission, with what meaning, and when, is a query against one hash-chained store rather than a search across mail threads and shared drives.

This is also where a common inspection question gets answered cheaply. An inspector who asks “prove that this specific PDF is the one Dr. Smith approved on this date” is asking for exactly the join the audit log already supports: look up the signature record by record_sha256, confirm it matches a hash recomputed from the artifact on file today, and read off the signer, meaning, and timestamp attached to that hash. No part of that answer depends on trusting a filename or a folder location — the hash is the only identifier that matters, which is precisely why it has to be computed correctly and early in the pipeline rather than assumed from context.

Error categorization and recovery

Signature failures split cleanly into permanent defects in the input and transient faults in the signing infrastructure, and conflating the two either blocks a submission that could be retried or, worse, silently retries a submission that should be rejected outright.

  • Missing manifestation content (permanent). A signer with no recorded meaning, or a meaning outside the four §11.50 values, is a defect in the request, not the signing service. Reject it before it reaches the KMS call; do not default a missing meaning to “approval.”
  • Identity components incomplete (permanent). A signer who has not passed the second, independent identification component fails §11.200 outright. This must fail closed — never sign on a single factor “temporarily.”
  • Hash mismatch after signing (permanent, and urgent). If a later verification finds the record’s hash no longer matches what was signed, the record changed after signing. Treat this as an integrity incident, not a retry candidate: quarantine the artifact and open an investigation rather than re-signing silently.
  • KMS or signing backend unavailable (transient). A network timeout or throttling response from the KMS is retryable with bounded backoff, the same pattern used for fallback routing elsewhere in the platform. The manifestation content itself does not need to be recomputed on retry, only the signing call.

Compliance checklist

Confirm each of the following before a signed artifact advances to transport:

  • The signer’s printed name is present in both the manifestation and the audit record
  • The date and time of signing is recorded in UTC with no ambiguity
  • A single value from the defined set of signature meanings is recorded, never a free-text substitute
  • The signer presented two distinct, verified identification components before signing
  • The signature covers the SHA-256 hash of the exact bytes transmitted, computed before any stamping
  • Re-hashing the record and re-verifying the signature after signing produces a match
  • The signing private key never enters application memory outside the KMS boundary
  • An audit event records the signer, meaning, hash, and timestamp for every signature produced

FAQ

Does a visible signature block on a PDF satisfy 21 CFR Part 11?

A visible block satisfies the human-readable portion of §11.50 — printed name, date and time, and meaning — but only if it is paired with a cryptographic signature bound to the record’s hash. A stamp with no underlying cryptographic linkage can be copied onto another document undetected, which is exactly what §11.70 prohibits.

Can the same signature be reused across multiple documents?

No. Because the signature is produced over the SHA-256 hash of one specific record’s bytes, it will fail verification against any other document, including a near-identical revision of the same file. That non-transferability is the practical effect of linking the signature to its record under §11.70.

What counts as the two identification components under 21 CFR 11.200?

The regulation requires at least two distinct components unless the signature is biometric. In practice this is commonly a unique identifier (a user ID) plus a password, or a unique identifier plus a biometric factor. The Python model in this guide represents that as a signer identity object that raises unless both a unique identifier and a verified credential are present.

Where does signing fit relative to eCTD assembly and technical validation?

Signing runs after a sequence has been assembled and has cleared technical validation, and before transport. Signing an artifact that has not yet passed structural checks risks producing a valid signature over content that will still change, which forces a re-sign and wastes the signer’s original attestation.