# -*- 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-2022 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 'stringio'
require 'hexapdf/error'
module HexaPDF
module DigitalSignature
module Signing
# This class is used for creating a CMS SignedData binary data object, as needed for PDF
# signing.
#
# OpenSSL already provides the ability to access, sign and create such CMS objects but is
# limited in what it offers in terms of data added to it. Since HexaPDF needs to follow the
# PDF standard, it needs control over the created structure so as to make it compatible with
# the various requirements.
#
# As the created CMS object is only meant to be used in the context of PDF signing, it also
# restricts certain things, like allowing only a single signer.
#
# Additionally, only RSA signatures are currently supported!
#
# See: PDF1.7/2.0 s12.8.3.3, PDF2.0 s12.8.3.4, RFC5652, ETSI TS 102 778 Parts 1-4
class SignedDataCreator
# Creates a SignedDataCreator, sets the given attributes if they are not nil and then calls
# #create with the given data, type and block.
def self.create(data, type: :cms, **attributes, &block)
instance = new
attributes.each {|key, value| instance.send("#{key}=", value) unless value.nil? }
instance.create(data, type: type, &block)
end
# The OpenSSL certificate object which is used to sign the data.
attr_accessor :certificate
# The OpenSSL key object which is used for signing. Needs to correspond to #certificate.
#
# If the key is not set, a block for signing will need to be provided to #sign.
attr_accessor :key
# Array of additional OpenSSL certificate objects that should be included.
#
# Should include all certificates of the hierarchy of the signing certificate.
attr_accessor :certificates
# The digest algorithm that should be used. Defaults to 'sha256'.
#
# Allowed values: sha256, sha384, sha512.
attr_accessor :digest_algorithm
# The timestamp handler instance that should be used for timestamping.
attr_accessor :timestamp_handler
# Creates a new SignedData object.
#
# Use the attribute accessor methods to set the required attributes.
def initialize
@certificate = nil
@key = nil
@certificates = []
@digest_algorithm = 'sha256'
@timestamp_handler = nil
end
# Creates a CMS SignedData binary data object for the given data using the set attributes
# and returns it in DER-serialized form.
#
# If the #key attribute is not set, the digest algorithm and the already digested data to be
# signed is yielded and the block needs to return the signature.
#
# +type+::
# The type can either be :cms when creating standard PDF CMS signatures or :pades when
# creating PAdES compatible signatures. PAdES signatures are part of PDF 2.0.
def create(data, type: :cms, &block) # :yield: digested_data
signed_attrs = create_signed_attrs(data, signing_time: (type == :cms))
signature = digest_and_sign_data(set(*signed_attrs.value).to_der, &block)
unsigned_attrs = create_unsigned_attrs(signature)
signer_info = create_signer_info(signature, signed_attrs, unsigned_attrs)
signed_data = create_signed_data(signer_info)
create_content_info(signed_data)
end
private
# Creates the set of signed attributes for the signer information structure.
def create_signed_attrs(data, signing_time: true)
set(
attribute('content-type', oid('id-data')),
(attribute('id-signingTime', utc_time(Time.now.utc)) if signing_time),
attribute(
'message-digest',
binary(OpenSSL::Digest.digest(@digest_algorithm, data))
),
attribute(
'id-aa-signingCertificateV2',
sequence( # SigningCertificateV2
sequence( # Seq of ESSCertIDv2
sequence( # ESSCertIDv2
#TODO: Does not validate on ETSI checker if used, doesn't matter if SHA256 or 512
#oid('sha512'),
binary(OpenSSL::Digest.digest('sha256', @certificate.to_der)), # certHash
sequence( # issuerSerial
sequence( # issuer
implicit(4, sequence(@certificate.issuer)) # choice 4 directoryName
),
integer(@certificate.serial) # serial
)
)
)
)
)
)
end
# Creates the set of unsigned attributes for the signer information structure.
def create_unsigned_attrs(signature)
attrs = set
if @timestamp_handler
time_stamp_token = @timestamp_handler.sign(StringIO.new(signature),
[0, signature.size, 0, 0])
attrs.value << attribute('id-aa-timeStampToken', time_stamp_token)
end
attrs.value.empty? ? nil : attrs
end
# Creates a single attribute for use in the (un)signed attributes set.
def attribute(name, value)
sequence(
oid(name), # attrType
set(value) # attrValues
)
end
# Digests the data and then signs it using the assigned key, or if the key is not available,
# by yielding to the caller.
def digest_and_sign_data(data)
hash = OpenSSL::Digest.digest(@digest_algorithm, data)
if @key
@key.sign_raw(@digest_algorithm, hash)
else
yield(@digest_algorithm, hash)
end
end
# Creates a signer information structure containing the actual meat of the whole CMS object.
def create_signer_info(signature, signed_attrs, unsigned_attrs = nil)
certificate_pkey_algorithm = @certificate.public_key.oid
signature_algorithm = if certificate_pkey_algorithm == 'rsaEncryption'
sequence( # signatureAlgorithm
oid('rsaEncryption'), # algorithmID
null # params
)
else
raise HexaPDF::Error, "Unsupported key type/signature algorithm"
end
sequence(
integer(1), # version
sequence( # sid (choice: issuerAndSerialNumber)
@certificate.issuer, # issuer
integer(@certificate.serial) # serial
),
sequence( # digestAlgorithm
oid(@digest_algorithm), # algorithmID
null # params
),
implicit(0, signed_attrs), # signedAttrs 0 implicit
signature_algorithm, # signatureAlgorithm
binary(signature), # signature
(implicit(1, unsigned_attrs) if unsigned_attrs) # unsignedAttrs 1 implicit
)
end
# Creates the signed data structure which is the actual content of the CMS object.
def create_signed_data(signer_info)
certificates = set(*[@certificate, @certificates].flatten)
sequence(
integer(1), # version
set( # digestAlgorithms
sequence( # digestAlgorithm
oid(@digest_algorithm), # algorithmID
null # params
)
),
sequence( # encapContentInfo (detached signature)
oid('id-data') # eContentType
),
implicit(0, certificates), # certificates 0 implicit
set( # signerInfos
signer_info # signerInfo
)
)
end
# Creates the content info structure which is the main structure containing everything else.
def create_content_info(signed_data)
signed_data.tag = 0
signed_data.tagging = :EXPLICIT
signed_data.tag_class = :CONTEXT_SPECIFIC
sequence(
oid('id-signedData'), # contentType
signed_data # content 0 explicit
)
end
# Changes the given ASN1Data object to use implicit tagging with the given +tag+ and a tag
# class of :CONTEXT_SPECIFIC.
def implicit(tag, data)
data.tag = tag
data.tagging = :IMPLICIT
data.tag_class = :CONTEXT_SPECIFIC
data
end
# Creates an ASN.1 set instance.
def set(*contents, tag: nil, tagging: nil)
OpenSSL::ASN1::Set.new(contents.compact, *tag, *tagging)
end
# Creates an ASN.1 sequence instance.
def sequence(*contents, tag: nil, tagging: nil)
OpenSSL::ASN1::Sequence.new(contents.compact, *tag, *tagging)
end
# Mapping of ASN.1 object ID names to object ID strings.
OIDS = {
'content-type' => '1.2.840.113549.1.9.3',
'message-digest' => '1.2.840.113549.1.9.4',
'id-data' => '1.2.840.113549.1.7.1',
'id-signedData' => '1.2.840.113549.1.7.2',
'id-signingTime' => '1.2.840.113549.1.9.5',
'sha256' => '2.16.840.1.101.3.4.2.1',
'sha384' => '2.16.840.1.101.3.4.2.2',
'sha512' => '2.16.840.1.101.3.4.2.3',
'rsaEncryption' => '1.2.840.113549.1.1.1',
'id-aa-signingCertificate' => '1.2.840.113549.1.9.16.2.12',
'id-aa-timeStampToken' => '1.2.840.113549.1.9.16.2.14',
'id-aa-signingCertificateV2' => '1.2.840.113549.1.9.16.2.47',
}
# Creates an ASN.1 object ID instance for the given object ID name.
def oid(name)
OpenSSL::ASN1::ObjectId.new(OIDS[name])
end
# Creates an ASN.1 octet string instance.
def binary(str)
OpenSSL::ASN1::OctetString.new(str)
end
# Creates an ASN.1 integer instance.
def integer(int)
OpenSSL::ASN1::Integer.new(int)
end
# Creates an ASN.1 UTC time instance.
def utc_time(value)
OpenSSL::ASN1::UTCTime.new(value)
end
# Creates an ASN.1 null instance.
def null
OpenSSL::ASN1::Null.new(nil)
end
end
end
end
end