# -*- 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-2024 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/digital_signature/handler' module HexaPDF module DigitalSignature # The signature handler for PKCS#7 a.k.a. CMS signatures. Those include, for example, the # adbe.pkcs7.detached and ETSI.CAdES.detached sub-filters. # # See: PDF2.0 s12.8.3.3 class CMSHandler < Handler # Creates a new signature handler for the given signature dictionary. def initialize(signature_dict) super @pkcs7 = OpenSSL::PKCS7.new(signature_dict.contents) end # Returns the common name of the signer. def signer_name signer_certificate.subject.to_a.assoc("CN")&.[](1) || super end # Returns the time of signing. def signing_time signer_info.signed_time rescue super end # Returns the certificate chain. def certificate_chain @pkcs7.certificates end # Returns the signer certificate (an instance of OpenSSL::X509::Certificate). def signer_certificate info = signer_info certificate_chain.find {|cert| cert.issuer == info.issuer && cert.serial == info.serial } end # Returns the signer information object (an instance of OpenSSL::PKCS7::SignerInfo). def signer_info @pkcs7.signers.first end # Verifies the signature using the provided OpenSSL::X509::Store object. def verify(store, allow_self_signed: false) result = super signer_info = self.signer_info signer_certificate = self.signer_certificate certificate_chain = self.certificate_chain if certificate_chain.empty? result.log(:error, "No certificates found in signature") return result end if @pkcs7.signers.size != 1 result.log(:error, "Exactly one signer needed, found #{@pkcs7.signers.size}") end unless signer_certificate result.log(:error, "Signer serial=#{signer_info.serial} issuer=#{signer_info.issuer} " \ "not found in certificates stored in PKCS7 object") return result end key_usage = signer_certificate.extensions.find {|ext| ext.oid == 'keyUsage' } unless key_usage && key_usage.value.split(', ').include?("Digital Signature") result.log(:error, "Certificate key usage is missing 'Digital Signature'") end if signature_dict.signature_type == 'ETSI.RFC3161' # Getting the needed values is not directly supported by Ruby OpenSSL p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, '')) signed_data = p7.value[1].value[0] content_info = signed_data.value[2] content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value) digest_algorithm = content.value[2].value[0].value[0].value original_hash = content.value[2].value[1].value recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data) hash_valid = (original_hash == recomputed_hash) else data = signature_dict.signed_data hash_valid = true # hash will be checked by @pkcs7.verify end if hash_valid && @pkcs7.verify(certificate_chain, store, data, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY) result.log(:info, "Signature valid") else result.log(:error, "Signature verification failed") end result end end end end