# -*- encoding: utf-8; frozen_string_literal: true -*-
#
#--
# This file is part of HexaPDF.
#
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
# Copyright (C) 2014-2025 Thomas Leitner
#
# HexaPDF is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License version 3 as
# published by the Free Software Foundation with the addition of the
# following permission added to Section 15 as permitted in Section 7(a):
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
# INFRINGEMENT OF THIRD PARTY RIGHTS.
#
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with HexaPDF. If not, see .
#
# The interactive user interfaces in modified source and object code
# versions of HexaPDF must display Appropriate Legal Notices, as required
# under Section 5 of the GNU Affero General Public License version 3.
#
# In accordance with Section 7(b) of the GNU Affero General Public
# License, a covered work must retain the producer line in every PDF that
# is created or manipulated using HexaPDF.
#
# If the GNU Affero General Public License doesn't fit your need,
# commercial licenses are available at .
#++
require 'openssl'
require 'net/http'
require 'hexapdf/error'
require 'stringio'
module HexaPDF
module DigitalSignature
module Signing
# This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
# document. It is registered under the :timestamp name.
#
# The timestamp is provided by a timestamp authority and establishes the document contents at
# the time indicated in the timestamp. Timestamping a PDF document is usually done in context
# of long term validation but can also be done standalone.
#
# == Usage
#
# It is necessary to provide at least the URL of the timestamp authority server (TSA) via
# #tsa_url, everything else is optional and uses default values. The TSA server must not use
# authentication to be usable.
#
# Example:
#
# document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
class TimestampHandler
# The URL of the timestamp authority server.
#
# This value is required.
attr_accessor :tsa_url
# The hash algorithm to use for timestamping. Defaults to SHA512.
attr_accessor :tsa_hash_algorithm
# The policy OID to use for timestamping. Defaults to +nil+.
attr_accessor :tsa_policy_id
# The size of the serialized signature that should be reserved.
#
# If this attribute has not been set, an empty string will be signed using #sign to
# determine the signature size. Note thtat this will contact the TSA server!
#
# The size needs to be at least as big as the final signature, otherwise signing results in
# an error.
attr_writer :signature_size
# The reason for timestamping. If used, will be set on the signature dictionary.
attr_accessor :reason
# The timestamping location. If used, will be set on the signature dictionary.
attr_accessor :location
# The contact information. If used, will be set on the signature dictionary.
attr_accessor :contact_info
# Creates a new TimestampHandler with the given attributes.
def initialize(**arguments)
@signature_size = nil
arguments.each {|name, value| send("#{name}=", value) }
end
# Returns the size of the serialized signature that should be reserved.
def signature_size
@signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
end
# Finalizes the signature field as well as the signature dictionary before writing.
def finalize_objects(_signature_field, signature)
signature.document.version = '2.0'
signature[:Type] = :DocTimeStamp
signature[:Filter] = :'Adobe.PPKLite'
signature[:SubFilter] = :'ETSI.RFC3161'
signature[:Reason] = reason if reason
signature[:Location] = location if location
signature[:ContactInfo] = contact_info if contact_info
end
# Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
# given IO byte ranges.
def sign(io, byte_range)
hash_algorithm = tsa_hash_algorithm || 'SHA512'
digest = OpenSSL::Digest.new(hash_algorithm)
io.pos = byte_range[0]
digest << io.read(byte_range[1])
io.pos = byte_range[2]
digest << io.read(byte_range[3])
req = OpenSSL::Timestamp::Request.new
req.algorithm = hash_algorithm
req.message_imprint = digest.digest
req.policy_id = tsa_policy_id if tsa_policy_id
http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
'content-type' => 'application/timestamp-query')
if http_response.kind_of?(Net::HTTPOK)
response = OpenSSL::Timestamp::Response.new(http_response.body)
if response.status == 0
response.token.to_der
else
raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
end
else
raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
end
end
end
end
end
end