This C# sample verifies every signature and document time-stamp of a PDF file, the way a PDF reader does it, and shows what each check means.

What is verified

  1. Integrity: PdfSignatureInfo.SignatureIsValid, the signed data was not changed.
  2. Coverage: the last signature covers the whole file. Data appended after the last signature keeps the signature valid, but it is not signed.
  3. Certificate: the validity period at the signing time, the chain to a trusted root (the standard .NET X509Chain) and the revocation status (CRL and OCSP, with DigitalCertificate.VerifyDigitalCertificate).
  4. Time-stamp: the time of the time-stamp and whether it was altered.

C# code

/// <summary>
/// Verifies a PDF file completely and displays the result. It returns true when every check succeeded.
/// </summary>
static bool VerifyPdfFile(string pdfFile)
{
    byte[] pdf = File.ReadAllBytes(pdfFile);

    PdfSignature pdfSignature = new PdfSignature(LibrarySerialNumber);
    pdfSignature.LoadPdfDocument(pdf);

    Console.WriteLine("File: " + Path.GetFileName(pdfFile));
    Console.WriteLine("Pages: " + pdfSignature.DocumentProperties.NumberOfPages + ", certification level: " + pdfSignature.DocumentProperties.CertificationLevel);

    if (pdfSignature.DocumentProperties.DigitalSignatures.Count == 0)
    {
        Console.WriteLine("The document has no signatures." + Environment.NewLine);
        return false;
    }

    Console.WriteLine("Kind of the document (indicative): " + DescribeSignatureProfile(pdf, pdfSignature));

    bool allChecksPassed = true;
    long[] coveredLengths = GetSignedByteRangeEnds(pdf);
    int signatureNumber = 0;

    foreach (PdfSignatureInfo signatureInfo in pdfSignature.DocumentProperties.DigitalSignatures)
    {
        Console.WriteLine("  Signature field: " + signatureInfo.SignatureName + " (" + signatureInfo.SignatureType + ")");
        Console.WriteLine("    Hash algorithm: " + signatureInfo.HashAlgorithm + ", signature algorithm: " + signatureInfo.DigestAlgorithm + ", key: " + signatureInfo.SignatureAlgorithm);

        if (!string.IsNullOrEmpty(signatureInfo.SigningReason))
            Console.WriteLine("    Reason: " + signatureInfo.SigningReason);

        // For a time-stamped signature, the signature time is the time of the time-stamp (a trusted time); otherwise it is
        // the time declared by the signer, that anybody can change.
        string timeSource = signatureInfo.SignatureType == SignatureType.TimestampSignature ? " (time of the document time-stamp)"
            : signatureInfo.SignatureIsTimestamped ? " (from the time-stamp)" : " (declared by the signer)";
        Console.WriteLine("    Signature time: " + signatureInfo.SignatureTime.ToLocalTime() + timeSource);

        // CHECK 1: the integrity of the signature.
        Console.WriteLine("    [1] Integrity of the signature: " + (signatureInfo.SignatureIsValid ? "OK" : "FAILED"));
        allChecksPassed &= signatureInfo.SignatureIsValid;

        // CHECK 2: the signature covers the document up to the end of its revision. Only the last signature must cover the end of the file.
        bool isLastSignature = signatureNumber == coveredLengths.Length - 1;
        if (signatureNumber < coveredLengths.Length)
        {
            bool coversFileEnd = coveredLengths[signatureNumber] >= pdf.Length;
            Console.WriteLine("    [2] Signed bytes: " + coveredLengths[signatureNumber] + " of " + pdf.Length + (coversFileEnd ? " (the whole file)" : " (a part of the file)"));

            if (isLastSignature && !coversFileEnd)
            {
                Console.WriteLine("        WARNING: data was added after the last signature: it is not signed.");
                allChecksPassed = false;
            }
        }

        // CHECK 3: the signing certificate.
        allChecksPassed &= VerifyCertificate(signatureInfo.SignatureCertificate, signatureInfo.SignatureTime, signatureInfo.SignatureType == SignatureType.TimestampSignature);

        // CHECK 4: the time-stamp of the signature.
        if (signatureInfo.SignatureIsTimestamped)
        {
            Console.WriteLine("    Time-stamp: " + signatureInfo.TimestampInfo.SignatureTime.ToLocalTime() + ", altered: " + signatureInfo.TimestampInfo.IsTimestampAltered
                + ", by " + signatureInfo.TimestampInfo.TsaCertificate.GetNameInfo(X509NameType.SimpleName, false));
            allChecksPassed &= !signatureInfo.TimestampInfo.IsTimestampAltered;
        }

        signatureNumber++;
    }

    Console.WriteLine("  RESULT: " + (allChecksPassed ? "all the checks passed" : "at least one check failed"));
    Console.WriteLine();

    return allChecksPassed;
}

/// <summary>
/// Verifies a signing certificate: the validity period at the signing time, the chain and the revocation status.
/// </summary>
static bool VerifyCertificate(X509Certificate2 certificate, DateTime signatureTime, bool isTimestampAuthorityCertificate)
{
    bool passed = true;

    Console.WriteLine("    Certificate: " + certificate.Subject + (isTimestampAuthorityCertificate ? " (time-stamping authority)" : string.Empty));

    // The certificate must be valid at the moment of the signature (the signature time is UTC).
    bool validAtSigningTime = signatureTime.ToUniversalTime() >= certificate.NotBefore.ToUniversalTime() && signatureTime.ToUniversalTime() <= certificate.NotAfter.ToUniversalTime();
    Console.WriteLine("    [3] Valid at the signing time: " + (validAtSigningTime ? "OK" : "FAILED") + " (" + certificate.NotBefore.ToShortDateString() + " - " + certificate.NotAfter.ToShortDateString() + ")");
    passed &= validAtSigningTime;

    // The certificate chain, with the standard .NET class. The application decides which root certificates are trusted: a
    // self-signed certificate (as the sample certificate) is not trusted by the Windows certificate store.
    using (X509Chain chain = new X509Chain())
    {
        chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
        chain.ChainPolicy.VerificationTime = signatureTime.ToLocalTime();

        bool chainIsTrusted = chain.Build(certificate);
        Console.WriteLine("    [4] Chain to a trusted root: " + (chainIsTrusted ? "OK" : "NOT TRUSTED") + " (" + chain.ChainElements.Count + " certificate(s))");

        if (!chainIsTrusted)
        {
            foreach (X509ChainStatus status in chain.ChainStatus)
                Console.WriteLine("        " + status.Status + ": " + status.StatusInformation.Trim());
        }

        // A self-signed sample certificate is not trusted: it is reported, but it does not fail the verification of this sample.
        // In a real application decide here what to do.
    }

    // The revocation status, from the CRL and from the OCSP responder published in the certificate.
    // Valid: not revoked. Revoked. Unknown: the CRL or the OCSP responder is not available.
    // NotPresent: the certificate does not publish a CRL or an OCSP responder (as a self-signed certificate).
    // The check is made now: for the status at the signing time use the validation data embedded in a PAdES-LT document.
    CertificateStatus crlStatus = DigitalCertificate.VerifyDigitalCertificate(certificate, VerificationType.CRL);
    CertificateStatus ocspStatus = DigitalCertificate.VerifyDigitalCertificate(certificate, VerificationType.OCSP);
    Console.WriteLine("    [5] Revocation: CRL " + crlStatus + ", OCSP " + ocspStatus);

    if (crlStatus == CertificateStatus.Revoked || ocspStatus == CertificateStatus.Revoked)
        passed = false;

    return passed;
}

The trust decision (which root certificates are trusted) belongs to the application. The project also detects the kind of each signature: PKCS#7 or PAdES, time-stamped, with validation data, long-term archival.

Run the full sample. The complete Visual Studio project PDF Signature Verification is included in the library download, in the folder NET Sample Projects (.NET 8, .NET 9, .NET 10 and .NET 11) and in NET Framework Sample Projects (.NET Framework 4.6.2), with the test files and a test certificate. Without a serial number the library runs in demo mode. Download the library and the samples · User manual

Frequently asked questions

Is SignatureIsValid enough to accept a signature?

No. It verifies only the integrity of the signature. A complete verification also checks the trust of the certificate chain, the revocation status at the signing time and the coverage of the document, as this sample does.

Can I verify a document signed by other software?

Yes. The library reads the standard PDF signatures (PKCS#7 and PAdES) created by any software.