# -*- 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-2023 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 'hexapdf/dictionary'
require 'hexapdf/error'
module HexaPDF
module DigitalSignature
# Represents a digital signature that is used to authenticate a user and the contents of the
# document.
#
# == Signature Verification
#
# Verification of signatures is a complex topic and what counts as completely verified may
# differ from use-case to use-case. Therefore HexaPDF provides as much diagnostic information as
# possible so that the user can decide whether a signature is valid.
#
# By defining a custom signature handler based on BaseHandler or CMSHandler one is able to also
# customize the signature verification.
#
# See: PDF2.0 s12.8.1, HexaPDF::Type::AcroForm::SignatureField
class Signature < Dictionary
# Represents a transform parameters dictionary.
#
# The allowed fields depend on the transform method, so not all fields are available all the
# time.
#
# See: PDF2.0 s12.8.2.2, s12.8.2.3, s12.8.2.4
class TransformParams < Dictionary
define_type :TransformParams
define_field :Type, type: Symbol, default: type
# For DocMDP, also used by UR
define_field :P, type: [Integer, Boolean]
define_field :V, type: Symbol, allowed_values: [:'1.2', :'2.2']
# For UR
define_field :Document, type: PDFArray
define_field :Msg, type: String
define_field :Annots, type: PDFArray, version: '1.5'
define_field :Form, type: PDFArray, version: '1.5'
define_field :Signature, type: PDFArray
define_field :EF, type: PDFArray, version: '1.6'
# For FieldMDP
define_field :Action, type: Symbol, allowed_values: [:All, :Include, :Exclude]
define_field :Fields, type: PDFArray
private
# All values allowed for the /Annots field
FIELD_ANNOTS_ALLOWED_VALUES = [:Create, :Delete, :Modify, :Copy, :Import, :Online, :SummaryView]
# All values allowed for the /Form field
FIELD_FORM_ALLOWED_VALUES = [:Add, :Delete, :Fillin, :Import, :Export, :SubmitStandalone,
:SpawnTemplate, :BarcodePlaintext, :Online]
# All values allowed for the /EF field
FIELD_EF_ALLOWED_VALUES = [:Create, :Delete, :Modify, :Import]
def perform_validation #:nodoc:
super
# We need to perform the checks here since the values are arrays and not single elements
if (annots = self[:Annots]) && !(annots = annots.value - FIELD_ANNOTS_ALLOWED_VALUES).empty?
yield("Field /Annots contains invalid entries: #{annots.join(', ')}", true)
value[:Annots].value -= annots
end
if (form = self[:Form]) && !(form = form.value - FIELD_FORM_ALLOWED_VALUES).empty?
yield("Field /Form contains invalid entries: #{form.join(', ')}", true)
value[:Form].value -= form
end
if (ef = self[:EF]) && !(ef = ef.value - FIELD_EF_ALLOWED_VALUES).empty?
yield("Field /EF contains invalid entries: #{ef.join(', ')}", true)
value[:EF].value -= ef
end
end
end
# Represents a signature reference dictionary.
#
# See: PDF2.0 s12.8.1, HexaPDF::DigitalSignature::Signature
class SignatureReference < Dictionary
define_type :SigRef
define_field :Type, type: Symbol, default: type
define_field :TransformMethod, type: Symbol, required: true,
allowed_values: [:DocMDP, :UR, :FieldMDP]
define_field :TransformParams, type: Dictionary
define_field :Data, type: ::Object
define_field :DigestMethod, type: Symbol, version: '1.5',
allowed_values: [:MD5, :SHA1, :SHA256, :SHA384, :SHA512, :RIPEMD160]
private
def perform_validation #:nodoc:
super
if self[:TransformMethod] == :FieldMDP && !key?(:Data)
yield("Field /Data is required when /TransformMethod is /FieldMDP")
end
end
end
define_field :Type, type: Symbol, default: :Sig,
allowed_values: [:Sig, :DocTimeStamp]
define_field :Filter, type: Symbol
define_field :SubFilter, type: Symbol
define_field :Contents, type: PDFByteString
define_field :Cert, type: [PDFArray, PDFByteString]
define_field :ByteRange, type: PDFArray
define_field :Reference, type: PDFArray
define_field :Changes, type: PDFArray
define_field :Name, type: String
define_field :M, type: PDFDate
define_field :Location, type: String
define_field :Reason, type: String
define_field :ContactInfo, type: String
define_field :R, type: Integer
define_field :V, type: Integer, default: 0, version: '1.5'
define_field :Prop_Build, type: Dictionary, version: '1.5'
define_field :Prop_AuthTime, type: Integer, version: '1.5'
define_field :Prop_AuthType, type: Symbol, version: '1.5',
allowed_values: [:PIN, :Password, :Fingerprint]
# Returns the name of the person or authority that signed the document.
def signer_name
signature_handler.signer_name
end
# Returns the time of the signing.
def signing_time
signature_handler.signing_time
end
# Returns the reason for the signing.
def signing_reason
self[:Reason]
end
# Returns the location of the signing.
def signing_location
self[:Location]
end
# Returns the signature type based on the /SubFilter.
def signature_type
self[:SubFilter].to_s
end
# Returns the signature handler for this signature based on the /SubFilter entry.
def signature_handler
cache(:signature_handler) do
handler_class = document.config.constantize('signature.sub_filter_map', self[:SubFilter]) do
raise HexaPDF::Error, "No or unknown signature handler set: #{self[:SubFilter]}"
end
handler_class.new(self)
end
end
# Returns the raw signature value.
def contents
self[:Contents]
end
# Returns the signed data as indicated by the /ByteRange entry as binary string.
def signed_data
unless document.revisions.parser
raise HexaPDF::Error, "Can't load signed data without existing PDF file"
end
io = document.revisions.parser.io
data = ''.b
self[:ByteRange]&.each_slice(2) do |offset, length|
io.pos = offset
data << io.read(length)
end
data
end
# Returns a VerificationResult object with the verification information.
def verify(default_paths: true, trusted_certs: [], allow_self_signed: false)
store = OpenSSL::X509::Store.new
store.set_default_paths if default_paths
store.purpose = OpenSSL::X509::PURPOSE_SMIME_SIGN
trusted_certs.each {|cert| store.add_cert(cert) }
signature_handler.verify(store, allow_self_signed: allow_self_signed)
end
private
def perform_validation #:nodoc:
if (self[:SubFilter] == :'ETSI.CAdES.detached' || self[:SubFilter] == :'ETSI.RFC3161') &&
document.version < '2.0'
yield("Signature handler needs at least PDF version 2.0", true)
document.version = '2.0'
end
end
end
end
end