Spire.PDF is a professional PDF library applied to creating, writing, editing, handling and reading PDF files without any external dependencies. Get free and professional technical support for Spire.PDF for .NET, Java, Android, C++, Python.

Mon Aug 25, 2025 2:07 pm

Hello team,
I am trying to add timestamp to my pdf file and it fails silently, and outputs error only to console.

1. Is it possible to override failure logic? I would like to throw exception instead.
2. Is there something wrong with my code?

I am able to do TSA request using cmd
Code: Select all
C:\Users\spark\Desktop>openssl ts -query -data file.pdf -sha256 -cert -out request.tsq
Using configuration from /usr/ssl/openssl.cnf
                                                                                                                        C:\Users\spark\Desktop>curl -H "Content-Type: application/timestamp-query" --data-binary "@request.tsq" http://tsa.swisssign.net -o response.tsr                                                                                                  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  6929  100  6859  100    70  85222    869 --:--:-- --:--:-- --:--:-- 88833

C:\Users\spark\Desktop>openssl ts -reply -in response.tsr -text
Using configuration from /usr/ssl/openssl.cnf
Status info:
Status: Granted.                                                                                                        Status description: Operation Okay
Failure info: unspecified

TST info:
Version: 1
Policy OID: 2.16.756.1.89.1.1.3.5                                                                                       Hash Algorithm: sha256
Message data:
    0000 - 75 b1 62 98 78 0d 47 04-5c 8c ca 67 a7 a9 1d 78   u.b.x.G.\..g...x
    0010 - 1d 39 56 52 f3 4e ee f2-c7 71 10 f0 7a fb 4a 16   .9VR.N...q..z.J.
Serial number: 0x68DDC4FF2BC4E8E9
Time stamp: Aug 25 13:59:07 2025 GMT
Accuracy: unspecified seconds, 0x01F4 millis, unspecified micros
Ordering: no
Nonce: 0xB56282593BCC2C84                                                                                               TSA: DirName:/C=CH/O=SwissSign AG/CN=SwissSign ZertES TSA UNIT A 2024 - 2
Extensions:


Here is my code and console output.

Code: Select all
using Spire.Pdf;
using Spire.Pdf.Security;

namespace ConsoleApp1;

internal class Program
{
    static void Main(string[] args)
    {
        var parsedArgs = ParseArgs(args);

        if (!parsedArgs.ContainsKey("input") || !parsedArgs.ContainsKey("output"))
        {
            Console.WriteLine("Usage: app.exe --input <input.pdf> --output <output.pdf>");
            return;
        }

        Spire.Pdf.License.LicenseProvider.SetLicenseKey(@"xxx");

        string inputPath = Path.GetFullPath(parsedArgs["input"]);
        string outputPath = Path.GetFullPath(parsedArgs["output"]);

        var doc = new PdfDocument(inputPath);
        PdfCertificate cert = new PdfCertificate(parsedArgs["cert"],
            parsedArgs["password"]);

        var signature = new PdfSignature(doc, doc.Pages[0], cert, "TimestampSignature")
        {
            Bounds = new System.Drawing.RectangleF(0, 0, 0, 0),
            DocumentPermissions = PdfCertificationFlags.ForbidChanges
        };

        signature.ConfigureTimestamp("http://tsa.swisssign.net");

        doc.SaveToFile(outputPath);

        Console.WriteLine("Conversion done!");
    }

    static Dictionary<string, string> ParseArgs(string[] args)
    {
        var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        for (int i = 0; i < args.Length - 1; i++)
        {
            if (args[i].StartsWith("--"))
            {
                string key = args[i].Substring(2);
                string value = args[i + 1];
                dict[key] = value;
                i++; // skip value
            }
        }

        return dict;
    }
}



Code: Select all
C:\Users\spark\Desktop\tsa\publish>ConsoleApp1.exe --input file.pdf --output output.pdf --cert mycert.pfx --password ODk7jAE4jIoYiJ0eHbrlpzNP5C3
invalid tsa http://tsa.swisssign.net.response.code 128
   at Spire.Pdf.Interactive.DigitalSignatures.TSAHttpService.?(Byte[] A_0)
   at Spire.Pdf.Interactive.DigitalSignatures.TSAHttpService.Generate(Byte[] signature)
   at Spire.Pdf.Interactive.DigitalSignatures.PdfPKCS7Formatter.?.?(IDictionary A_0)
invalid tsa http://tsa.swisssign.net.response.code 128
   at Spire.Pdf.Interactive.DigitalSignatures.TSAHttpService.?(Byte[] A_0)
   at Spire.Pdf.Interactive.DigitalSignatures.TSAHttpService.Generate(Byte[] signature)
   at Spire.Pdf.Interactive.DigitalSignatures.PdfPKCS7Formatter.?.?(IDictionary A_0)
Conversion done!


Kind regards,
Yehor
User avatar

yegor.androsov
 
Posts: 6
Joined: Fri Aug 22, 2025 6:11 pm

Tue Aug 26, 2025 10:17 am

Hello,

Thank you for your inquiry.

I reproduced your issue and logged it into our bug tracking system with the ticket SPIREPDF-7693. Regarding your request for exception throwing, I have also logged an investigation task into our tracking using SPIREPDF-7465. Our dev team will investigate and fix it, once there is any update, I will give you feedback. Thank you for your understanding.
Sincerely,
Talia
E-iceblue support team
User avatar

talia.liu
 
Posts: 331
Joined: Mon Apr 14, 2025 3:33 am

Fri Oct 03, 2025 6:53 pm

Hello team, could you provide some information about possible timeline for this issue? Can we expect it to be fixed?
User avatar

yegor.androsov
 
Posts: 6
Joined: Fri Aug 22, 2025 6:11 pm

Tue Oct 07, 2025 2:28 am

Hello,

Thank you for your following up. Sorry there hasn't been much progress on these two issues yet. Our Dev team colleagues still need some time. Currently we are unable to provide an accurate ETA for you. I will urge them again, once there is further progress, we will update you. Thank you for your understanding.

Sincerely,
Lisa
E-iceblue support team
User avatar

Lisa.Li
 
Posts: 1510
Joined: Wed Apr 25, 2018 3:20 am

Thu Oct 23, 2025 9:48 am

Hello,

We are pleased to announce that our latest Spire.PDF Pack (Hot Fix) Version 11.10.4 has fixed issue SPIREPDF-7693 and introduced support for validating the timestamp service URL address(SPIREPDF-7465). Please refer to the following code for implementation:
Code: Select all
 TSAHttpService timestampService = new TSAHttpService("http://time2.certum.pl");

    TSAResponse response = timestampService.Check();

    //if it is success to receive tsa token
    if (response.Success)

    { formatter.TimestampService = timestampService; }

Welcome to test. We are looking forward to your testing feedback.
Website:
https://www.e-iceblue.com/Download/download-pdf-for-net-now.html
Nuget:
https://www.nuget.org/packages/Spire.PDF/11.10.4
https://www.nuget.org/packages/Spire.PDFfor.NETStandard/11.10.4
Sincerely,
Talia
E-iceblue support team
User avatar

talia.liu
 
Posts: 331
Joined: Mon Apr 14, 2025 3:33 am

Thu Oct 23, 2025 7:07 pm

Hi Talia,
Thank you for your response.

Unfortunately, I see multiple issues with this solution.
1. What is formatter? There was no formatter in my code. I think you are referring to PdfPKCS7Formatter but I could not find where I can update its settings.
2. Your implementation generates random bytes and sends to TSA. Our provider charges us per HTTP calls made and this solution will automatically double our quota usage. I am referring to the code below.
Code: Select all
    public TSAResponse Check()
    {
      TSAResponse tsaResponse = new TSAResponse();
      try
      {
        byte[] numArray = new byte[100];
        new Random(1).NextBytes(numArray); // <--- i am referring to this line
        byte[] A_0 = spr\u25F2.蒫(this.蛗, numArray);
        tsaResponse.Token = this.蔶(A_0);
        tsaResponse.Success = true;
      }
      catch (Exception ex)
      {
        tsaResponse.Message = ex.Message;
        tsaResponse.Success = false;
      }
      return tsaResponse;
    }


My expected flow stays the same - document is configured with TSA and an actual HTTP call is delayed until save. If TSA request fails, the exception should be thrown. This is a breaking change, obviously, but I don't see how anyone would want to configure TSA endpoint and then it might fail silently and give no exception.
This solves the problem I mentioned earlier, one HTTP call is used to timestamp one document.

I strongly suggest you to update the implementation of this class instead. It uses outdated code structures (goto/labels) and suppresses the error. Or at least give the possibility to replace it with our own implementation that throws an error.
Code: Select all
    internal class 蔶 : spr渶
    {
      private const string 蒫 = "1.2.840.113549.1.9.16.2.14";
      private readonly ITSAService 蔶;

      public 蔶(ITSAService A_0) => this.蔶 = A_0;

      public spr풖 ぃ(IDictionary A_0)
      {
        int A_1 = 19;
        byte[] signature = A_0[(object) spr涫.藁] as byte[];
        byte[] buffer = (byte[]) null;
        if (this.蔶 != null)
          goto label_2;
label_1:
        if (buffer == null)
          return (spr풖) null;
        spr\u1717 spr = new spr\u1717((Stream) new MemoryStream(buffer));
        spr홶 A_0_1 = new spr홶();
        spr홶 A_0_2 = new spr홶();
        A_0_2.蒫((spr헫) new sprㆲ(PackageAttribute.b("įᰱسᠵ7ู\u0C3Bွ焿獁睃獅籇獉手罍繏歑穓杕湗瑙湛灝兟噡", A_1)));
        spr\u1AE4 A_0_3 = (spr\u1AE4) spr.蒫();
        A_0_2.蒫((spr헫) new spr㘊((spr헫) A_0_3));
        A_0_1.蒫((spr헫) new spr㑩(A_0_2));
        return new spr풖(A_0_1);
label_2:
        try
        {
          buffer = this.蔶.Generate(signature);
          goto label_1;
        }
        catch (Exception ex)
        {
          Console.Out.WriteLine(ex.Message);
          Console.Out.WriteLine(ex.StackTrace);
          goto label_1;
        }
      }
    }
User avatar

yegor.androsov
 
Posts: 6
Joined: Fri Aug 22, 2025 6:11 pm

Thu Oct 23, 2025 7:27 pm

Despite the additional comments from me, Timestamp is properly assigned displayed in document properties in the new version.
Thanks a lot.
However, it would be great, if it would throw an error if the TSA is not available or misconfigured for reasons mentioned previously
User avatar

yegor.androsov
 
Posts: 6
Joined: Fri Aug 22, 2025 6:11 pm

Fri Oct 24, 2025 10:17 am

Hello,

Thank you for your feedback and suggestions.
Please refer to the following code to implement a custom timestamp acquisition service class:
Code: Select all
PdfDocument doc = new PdfDocument();
        doc.AppendPage();

        X509Certificate2 x509 = new X509Certificate2(@"D:\workspace\MyConsoleApp\gary.pfx", "e-iceblue");
        Test.TSAHttpService tsaHttpService = new Test.TSAHttpService("http://time.certum.pl");
        PdfPKCS7Formatter formatter = new PdfPKCS7Formatter(x509, false);
        formatter.TimestampService = tsaHttpService;

        // Create a signature maker using the loaded document and formatter
        PdfOrdinarySignatureMaker signatureMaker = new PdfOrdinarySignatureMaker(doc, formatter);

        // Create a custom signature appearance for the signature
        IPdfSignatureAppearance signatureAppearance = new PdfCustomSignatureAppearance();

        // Make the signature on the first page of the document at position (100, 100) with a size of 100x100
        signatureMaker.MakeSignature("sign", doc.Pages[0], 100, 100, 100, 100, signatureAppearance);

        // Save the result pdf file
        doc.SaveToFile(@"D:\workspace\MyConsoleApp\result.pdf", FileFormat.PDF);
        doc.Close();
        if(null != tsaHttpService.LastResponse && !tsaHttpService.LastResponse.Success)
       

{             throw new Exception(tsaHttpService.LastResponse.Message);         }


Code: Select all
using Org.BouncyCastle.Asn1.Cmp;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Tsp;
using System.Globalization;
using System.Net;
using System.Text;

namespace Test
{
    /// <summary>
    /// Time Stamp service implementation which must conform to RFC 3161.
    /// </summary>
    public class TSAHttpService : Spire.Pdf.Interactive.DigitalSignatures.ITSAService
    {
        // The default value for the hash algorithm
        //private const int DEFAULTTOKENSIZE = 4096;

        // The default value for the hash algorithm
        private const String DEFAULTHASHALGORITHM = "SHA256";

        // URL of the Time Stamp Authority
        private String m_url;

        // TSA Username
        private String m_user;

        // TSA password
        private String m_password;
       
        // Estimate of the received time stamp token
        //private int m_tokenSizeEstimate;
       
        // Hash algorithm
        private String m_digestAlgorithm;

        /// <summary>
        /// Construct an instance of a TSAClient.
        /// </summary>
        /// <param name="url">Time Stamp Authority URL.</param>
        public TSAHttpService(String url)
            : this(url, null, null, DEFAULTHASHALGORITHM)
       

{         }

        /// <summary>
        /// Construct an instance of a TSAClient.
        /// </summary>
        /// <param name="url">Time Stamp Authority URL.</param>
        /// <param name="user">user(account).</param>
        /// <param name="password">password.</param>
        public TSAHttpService(String url, String user, String password)
            : this(url, user, password, DEFAULTHASHALGORITHM)
        {         }
        // <summary>
        // Construct an instance of a TSAClient.
        // Note the token size estimate is updated by each call, as the token
        // size is not likely to change(as long as we call the same TSA using
        // the same imprint length).
        // </summary>
        // <param name="url">Time Stamp Authority URL.</param>
        // <param name="user">user(account).</param>
        // <param name="password">password</param>
        // <param name="tokenSizeEstimate">estimated size of received time stamp token (DER encoded).</param>
        // <param name="digestAlgorithm"></param>
        private TSAHttpService(String url, String user, String password, String digestAlgorithm)
       

{             this.m_url = url;             this.m_user = user;             this.m_password = password;             //this.m_tokenSizeEstimate = tokenSizeEstimate;             this.m_digestAlgorithm = digestAlgorithm;         }
        /// <summary>
        /// Check the time Stamp service.
        /// if the service is not validated, throw exception.
        /// </summary>
        public TSAResponse Check()
        {
            TSAResponse response = new TSAResponse();
            try
           

{                 byte[] randomBytes = new byte[100];                 Random random = new Random(1);                 random.NextBytes(randomBytes);                     byte[] imprint = Org.BouncyCastle.Security.DigestUtilities.CalculateDigest(m_digestAlgorithm, randomBytes);                 response.Token = GetTimeStampToken(imprint);                 response.Success = true;             }
            catch(Exception ex)
           

{                 response.Message = ex.Message;                 response.Success = false;             }
            return response;
        }

        /// <summary>
        /// Generate timestamp token.
        /// </summary>
        /// <param name="signature">
        /// The value of signature field within SignerInfo.
        /// The value of messageImprint field within TimeStampToken shall be the hash of signature.
        /// Refrence RFC 3161 APPENDIX A.
        /// </param>
        /// <returns>timestamp which must conform to RFC 3161</returns>
        public byte[] Generate(byte[] signature)
        {
            byte[] timeStampToken = null;
            try
           

{                 byte[] imprint = Org.BouncyCastle.Security.DigestUtilities.CalculateDigest(m_digestAlgorithm, signature);                 timeStampToken = GetTimeStampToken(imprint);             }
            catch (Exception ex)
           

{                 LastResponse = new TSAResponse();                 LastResponse.Message = ex.Message;                 LastResponse.Success = false;             }
            return timeStampToken;
        }

        public TSAResponse LastResponse { get; set; }

        // <summary>
        // Get RFC 3161 timeStampToken.
        // Method may return null indicating that timestamp should be skipped.
        // </summary>
        // <param name="imprint">data imprint to be time-stamped</param>
        // <returns>encoded TSA signed data of the timeStampToken</returns>
        private byte[] GetTimeStampToken(byte[] imprint)
        {
            byte[] respBytes = null;
            // Setup the time stamp request
            TimeStampRequestGenerator tsqGenerator = new TimeStampRequestGenerator();
            tsqGenerator.SetCertReq(true);
            // tsqGenerator.setReqPolicy("1.3.6.1.4.1.601.10.3.1");
            BigInteger nonce = BigInteger.ValueOf(DateTime.Now.Ticks + Environment.TickCount);
            TimeStampRequest request = tsqGenerator.Generate(Org.BouncyCastle.Security.DigestUtilities.GetObjectIdentifier(m_digestAlgorithm), imprint, nonce);
            byte[] requestBytes = request.GetEncoded();

            // Call the communications layer
            respBytes = GetTSAResponse(requestBytes);

            // Handle the TSA response
            TimeStampResponse response = new TimeStampResponse(respBytes);

            // validate communication level attributes (RFC 3161 PKIStatus)
            response.Validate(request);
            PkiFailureInfo failure = response.GetFailInfo();
            int value = (failure == null) ? 0 : failure.IntValue;
            if (value != 0)
           

{                 // @todo: Translate value of 15 error codes defined by PKIFailureInfo to string                 throw new IOException("fail: " + response.GetStatusString());             }
            // @todo: validate the time stap certificate chain (if we want
            //        assure we do not sign using an invalid timestamp).

            // extract just the time stamp token (removes communication status info)
            TimeStampToken tsToken = response.TimeStampToken;
            if (tsToken == null)
           

{                 throw new IOException("fail: time stamp token is null." + response.GetStatusString());             }
            TimeStampTokenInfo tsTokenInfo = tsToken.TimeStampInfo; // to view details
            byte[] encoded = tsToken.GetEncoded();

            //LOGGER.Info("Timestamp generated: " + tsTokenInfo.GenTime);
            //if (tsaInfo != null)

{             //    tsaInfo.InspectTimeStampTokenInfo(tsTokenInfo);             //}
            // Update our token size estimate for the next call (padded to be safe)
            //this.m_tokenSizeEstimate = encoded.Length + 32;
            return encoded;
        }

        // <summary>
        //
        // </summary>
        // <param name="requestBytes"></param>
        // <returns>TSA response, raw bytes (RFC 3161 encoded)</returns>
        private byte[] GetTSAResponse(byte[] requestBytes)
        {
            HttpWebRequest con = (HttpWebRequest)WebRequest.Create(m_url);
            con.ContentLength = requestBytes.Length;
            con.ContentType = "application/timestamp-query";
            con.Method = "POST";
            if ((m_user != null) && !m_user.Equals(""))
           

{                 string authInfo = m_user + ":" + m_password;                 authInfo = Convert.ToBase64String(Encoding.Default.GetBytes(authInfo), Base64FormattingOptions.None);                 con.Headers["Authorization"] = "Basic " + authInfo;             }
            Stream outp = con.GetRequestStream();
            outp.Write(requestBytes, 0, requestBytes.Length);
            outp.Close();
            HttpWebResponse response = (HttpWebResponse)con.GetResponse();
            if (response.StatusCode != HttpStatusCode.OK)
                throw new IOException("invalid.http.response." + (int)response.StatusCode);
            Stream inp = response.GetResponseStream();

            MemoryStream baos = new MemoryStream();
            byte[] buffer = new byte[1024];
            int bytesRead = 0;
            while ((bytesRead = inp.Read(buffer, 0, buffer.Length)) > 0)
           

{                 baos.Write(buffer, 0, bytesRead);             }
            byte[] respBytes = baos.ToArray();
            inp.Close();

            String encoding = response.ContentEncoding;
            response.Close();
            if (encoding != null && 0 == CultureInfo.InvariantCulture.CompareInfo.Compare(encoding, "base64", CompareOptions.IgnoreCase))
           

{                 respBytes = Convert.FromBase64String(Encoding.ASCII.GetString(respBytes));             }
            return respBytes;
        }
    }
}

If the above methods do not resolve your issue, please provide a screenshot of your expected output results, and we will conduct further investigation.
Sincerely,
Talia
E-iceblue support team
User avatar

talia.liu
 
Posts: 331
Joined: Mon Apr 14, 2025 3:33 am

Return to Spire.PDF