adjust verify signature
This commit is contained in:
@@ -4,16 +4,16 @@ class VerifyPdfSignatureController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
pdfs =
|
||||
params[:files].map do |file|
|
||||
HexaPDF::Document.new(io: file.open)
|
||||
end
|
||||
|
||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||
|
||||
signatures =
|
||||
params[:files].map do |file|
|
||||
VerifyPdfSignature.call(file.open, trusted_certs)
|
||||
end
|
||||
|
||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
locals: { signatures:, files: params[:files] })
|
||||
rescue Pdfium::PdfiumError
|
||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div id="result">
|
||||
<% files.zip(pdfs).each do |file, pdf| %>
|
||||
<% files.zip(signatures).each do |file, file_signatures| %>
|
||||
<div class="mb-4 border border-base-300 rounded-md py-2 px-3">
|
||||
<% if pdf.signatures.to_a.size == 0 %>
|
||||
<% if file_signatures.size == 0 %>
|
||||
<div class="text-sm">
|
||||
<%= file.original_filename %>
|
||||
</div>
|
||||
@@ -11,42 +11,29 @@
|
||||
<% else %>
|
||||
<div class="flex items-center space-x-1 border-b border-dashed border-base-300 pb-2">
|
||||
<%= svg_icon('file_text', class: 'w-5 h-5 inline') %>
|
||||
<span><%= file.original_filename %> - <%= pluralize(pdf.signatures.to_a.size, 'Signature') %></span>
|
||||
<span><%= file.original_filename %> - <%= pluralize(file_signatures.size, 'Signature') %></span>
|
||||
</div>
|
||||
<% pdf.signatures.to_a.each do |signature| %>
|
||||
<% file_signatures.each do |signature| %>
|
||||
<div class="mt-3">
|
||||
<div class="space-y-1 font-medium pb-2 text-xl">
|
||||
<% signature.verify(trusted_certs:).messages.map(&:content).each do |message| %>
|
||||
<% signature.messages.each do |message| %>
|
||||
<p class="flex space-x-1 items-center">
|
||||
<% if message == 'Signature verification failed' %>
|
||||
<% if message.status == :error %>
|
||||
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||
<% elsif message == 'Signature valid' %>
|
||||
<% elsif message.status == :success %>
|
||||
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
|
||||
<% elsif message.status == :warning %>
|
||||
<%= svg_icon('alert_circle', class: 'w-6 h-6 text-yellow-500') %>
|
||||
<% end %>
|
||||
<span>
|
||||
<%= message %>
|
||||
<%= message.text %>
|
||||
</span>
|
||||
</p>
|
||||
<% if message == 'Signature valid' %>
|
||||
<p class="flex space-x-1 items-center">
|
||||
<% if trusted_certs.any? { |e| e.public_key.to_der == signature.signature_handler.signer_certificate.public_key.to_der } %>
|
||||
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
|
||||
<span>
|
||||
<%= t('signed_with_trusted_certificate') %>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||
<span>
|
||||
<%= t('signed_with_external_certificate') %>
|
||||
</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('user', class: 'w-5 h-5 inline') %>
|
||||
<span><%= signature.signing_reason %></span>
|
||||
<span><%= signature.reason %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('calendar', class: 'w-5 h-5 inline') %>
|
||||
@@ -54,11 +41,11 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
|
||||
<span><%= signature.signer_name.force_encoding('UTF-8') %></span>
|
||||
<span><%= signature.common_name %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>
|
||||
<span><%= signature.signature_type %></span>
|
||||
<span><%= signature.type %></span>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -596,6 +596,9 @@ en: &en
|
||||
there_are_no_signatures: There are no signatures
|
||||
signed_with_trusted_certificate: Signed with trusted certificate
|
||||
signed_with_external_certificate: Signed with external certificate
|
||||
signature_valid: Signature valid
|
||||
signature_verification_failed: Signature verification failed
|
||||
contains_unsigned_changes_after_the_last_signature: Contains unsigned changes after the last signature
|
||||
setup_2fa_to_continue: Setup 2FA to continue.
|
||||
create_a_new_account: Create a new account.
|
||||
you_are_already_signed_in: You are already signed in.
|
||||
@@ -1706,6 +1709,9 @@ es: &es
|
||||
there_are_no_signatures: No hay firmas
|
||||
signed_with_trusted_certificate: Firmado con certificado de confianza
|
||||
signed_with_external_certificate: Firmado con certificado externo
|
||||
signature_valid: Firma válida
|
||||
signature_verification_failed: Verificación de firma fallida
|
||||
contains_unsigned_changes_after_the_last_signature: Contiene cambios no firmados después de la última firma
|
||||
setup_2fa_to_continue: Configura 2FA para continuar.
|
||||
create_a_new_account: Crear una nueva cuenta.
|
||||
you_are_already_signed_in: Ya has iniciado sesión.
|
||||
@@ -2827,6 +2833,9 @@ it: &it
|
||||
there_are_no_signatures: Non ci sono firme
|
||||
signed_with_trusted_certificate: Firmato con certificato affidabile
|
||||
signed_with_external_certificate: Firmato con certificato esterno
|
||||
signature_valid: Firma valida
|
||||
signature_verification_failed: Verifica della firma non riuscita
|
||||
contains_unsigned_changes_after_the_last_signature: Contiene modifiche non firmate dopo l’ultima firma
|
||||
setup_2fa_to_continue: Configura 2FA per continuare.
|
||||
create_a_new_account: Crea un nuovo account.
|
||||
you_are_already_signed_in: "Hai già effettuato l'accesso."
|
||||
@@ -3934,6 +3943,9 @@ fr: &fr
|
||||
there_are_no_signatures: Il n’y a pas de signatures
|
||||
signed_with_trusted_certificate: Signé avec un certificat de confiance
|
||||
signed_with_external_certificate: Signé avec un certificat externe
|
||||
signature_valid: Signature valide
|
||||
signature_verification_failed: Échec de la vérification de la signature
|
||||
contains_unsigned_changes_after_the_last_signature: Contient des modifications non signées après la dernière signature
|
||||
setup_2fa_to_continue: Configurez la 2FA pour continuer.
|
||||
create_a_new_account: Créer un nouveau compte.
|
||||
you_are_already_signed_in: Vous êtes déjà connecté.
|
||||
@@ -5051,6 +5063,9 @@ pt: &pt
|
||||
there_are_no_signatures: Não há assinaturas
|
||||
signed_with_trusted_certificate: Assinado com certificado confiável
|
||||
signed_with_external_certificate: Assinado com certificado externo
|
||||
signature_valid: Assinatura válida
|
||||
signature_verification_failed: Falha na verificação da assinatura
|
||||
contains_unsigned_changes_after_the_last_signature: Contém alterações não assinadas após a última assinatura
|
||||
setup_2fa_to_continue: Configure 2FA para continuar.
|
||||
create_a_new_account: Criar uma nova conta.
|
||||
you_are_already_signed_in: Você já está conectado.
|
||||
@@ -6171,6 +6186,9 @@ de: &de
|
||||
there_are_no_signatures: Es gibt keine Unterschriften
|
||||
signed_with_trusted_certificate: Signiert mit vertrauenswürdigem Zertifikat
|
||||
signed_with_external_certificate: Signiert mit externem Zertifikat
|
||||
signature_valid: Signatur gültig
|
||||
signature_verification_failed: Signaturprüfung fehlgeschlagen
|
||||
contains_unsigned_changes_after_the_last_signature: Enthält nicht signierte Änderungen nach der letzten Signatur
|
||||
setup_2fa_to_continue: Richten Sie 2FA ein, um fortzufahren.
|
||||
create_a_new_account: Neues Konto erstellen.
|
||||
you_are_already_signed_in: Sie sind bereits angemeldet.
|
||||
@@ -7807,6 +7825,9 @@ nl: &nl
|
||||
there_are_no_signatures: Er zijn geen handtekeningen
|
||||
signed_with_trusted_certificate: Ondertekend met vertrouwd certificaat
|
||||
signed_with_external_certificate: Ondertekend met extern certificaat
|
||||
signature_valid: Handtekening geldig
|
||||
signature_verification_failed: Verificatie van handtekening mislukt
|
||||
contains_unsigned_changes_after_the_last_signature: Bevat niet-ondertekende wijzigingen na de laatste handtekening
|
||||
setup_2fa_to_continue: Stel 2FA in om door te gaan.
|
||||
create_a_new_account: Maak een nieuw account aan.
|
||||
you_are_already_signed_in: U bent al aangemeld.
|
||||
|
||||
+197
@@ -28,6 +28,10 @@ class Pdfium
|
||||
typedef :pointer, :FPDF_PAGEOBJECT
|
||||
typedef :pointer, :FPDF_PATHSEGMENT
|
||||
typedef :pointer, :FPDF_FONT
|
||||
typedef :pointer, :FPDF_SIGNATURE
|
||||
typedef :pointer, :FPDF_ANNOTATION
|
||||
typedef :pointer, :FPDF_BOOKMARK
|
||||
typedef :pointer, :FPDF_DEST
|
||||
|
||||
MAX_SIZE = 32_767
|
||||
|
||||
@@ -102,6 +106,31 @@ class Pdfium
|
||||
attach_function :FPDF_CloseDocument, [:FPDF_DOCUMENT], :void
|
||||
attach_function :FPDF_GetPageCount, [:FPDF_DOCUMENT], :int
|
||||
attach_function :FPDF_GetLastError, [], :ulong
|
||||
attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
|
||||
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
|
||||
|
||||
attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :int
|
||||
attach_function :FPDFPage_GetAnnot, %i[FPDF_PAGE int], :FPDF_ANNOTATION
|
||||
attach_function :FPDFPage_CloseAnnot, [:FPDF_ANNOTATION], :void
|
||||
attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetRect, %i[FPDF_ANNOTATION pointer], :int
|
||||
|
||||
attach_function :FPDFBookmark_GetFirstChild, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_BOOKMARK
|
||||
attach_function :FPDFBookmark_GetNextSibling, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_BOOKMARK
|
||||
attach_function :FPDFBookmark_GetTitle, %i[FPDF_BOOKMARK pointer ulong], :ulong
|
||||
attach_function :FPDFBookmark_GetDest, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_DEST
|
||||
attach_function :FPDFDest_GetDestPageIndex, %i[FPDF_DOCUMENT FPDF_DEST], :int
|
||||
attach_function :FPDFDest_GetLocationInPage,
|
||||
%i[FPDF_DEST pointer pointer pointer pointer pointer pointer], :int
|
||||
|
||||
attach_function :FPDF_GetSignatureCount, [:FPDF_DOCUMENT], :int
|
||||
attach_function :FPDF_GetSignatureObject, %i[FPDF_DOCUMENT int], :FPDF_SIGNATURE
|
||||
attach_function :FPDFSignatureObj_GetContents, %i[FPDF_SIGNATURE pointer ulong], :ulong
|
||||
attach_function :FPDFSignatureObj_GetByteRange, %i[FPDF_SIGNATURE pointer ulong], :ulong
|
||||
attach_function :FPDFSignatureObj_GetSubFilter, %i[FPDF_SIGNATURE pointer ulong], :ulong
|
||||
attach_function :FPDFSignatureObj_GetReason, %i[FPDF_SIGNATURE pointer ulong], :ulong
|
||||
attach_function :FPDFSignatureObj_GetTime, %i[FPDF_SIGNATURE pointer ulong], :ulong
|
||||
attach_function :FPDFSignatureObj_GetDocMDPPermission, [:FPDF_SIGNATURE], :uint
|
||||
|
||||
attach_function :FPDF_LoadPage, %i[FPDF_DOCUMENT int], :FPDF_PAGE
|
||||
attach_function :FPDF_ClosePage, [:FPDF_PAGE], :void
|
||||
@@ -253,6 +282,13 @@ class Pdfium
|
||||
FLATTEN_NOTHINGTODO = 2
|
||||
|
||||
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||
class FS_RECTF < FFI::Struct
|
||||
layout :left, :float,
|
||||
:top, :float,
|
||||
:right, :float,
|
||||
:bottom, :float
|
||||
end
|
||||
|
||||
class FS_MATRIX < FFI::Struct
|
||||
layout :a, :float,
|
||||
:b, :float,
|
||||
@@ -519,6 +555,72 @@ class Pdfium
|
||||
@pages[page_index] ||= Page.new(self, page_index)
|
||||
end
|
||||
|
||||
def bookmarks(parent = nil, seen = Set.new)
|
||||
acc = []
|
||||
bookmark = Pdfium.FPDFBookmark_GetFirstChild(@document_ptr, parent)
|
||||
|
||||
until bookmark.null?
|
||||
break unless seen.add?(bookmark.address)
|
||||
|
||||
acc << [bookmark_title(bookmark), *destination(Pdfium.FPDFBookmark_GetDest(@document_ptr, bookmark))]
|
||||
acc.concat(bookmarks(bookmark, seen))
|
||||
|
||||
bookmark = Pdfium.FPDFBookmark_GetNextSibling(@document_ptr, bookmark)
|
||||
end
|
||||
|
||||
acc
|
||||
end
|
||||
|
||||
def bookmark_title(bookmark)
|
||||
length = Pdfium.FPDFBookmark_GetTitle(bookmark, nil, 0)
|
||||
|
||||
return if length.zero?
|
||||
|
||||
buffer = FFI::MemoryPointer.new(:char, length)
|
||||
Pdfium.FPDFBookmark_GetTitle(bookmark, buffer, length)
|
||||
|
||||
buffer.read_bytes(length).force_encoding('UTF-16LE').encode('UTF-8').delete("\u0000")
|
||||
end
|
||||
|
||||
def destination(dest)
|
||||
return [] if dest.nil? || dest.null?
|
||||
|
||||
flags = Array.new(3) { FFI::MemoryPointer.new(:int) }
|
||||
coords = Array.new(3) { FFI::MemoryPointer.new(:float) }
|
||||
Pdfium.FPDFDest_GetLocationInPage(dest, *flags, *coords)
|
||||
|
||||
[Pdfium.FPDFDest_GetDestPageIndex(@document_ptr, dest),
|
||||
*coords.each_with_index.map { |c, i| c.read_float.round(3) if flags[i].read_int == 1 }]
|
||||
end
|
||||
|
||||
def valid_cross_reference_table?
|
||||
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
|
||||
end
|
||||
|
||||
def signature_count
|
||||
@signature_count ||= Pdfium.FPDF_GetSignatureCount(@document_ptr)
|
||||
end
|
||||
|
||||
def signatures
|
||||
@signatures ||= (0...signature_count).map { |index| Signature.new(self, index) }
|
||||
end
|
||||
|
||||
def trailer_ends
|
||||
@trailer_ends ||=
|
||||
begin
|
||||
count = Pdfium.FPDF_GetTrailerEnds(@document_ptr, nil, 0)
|
||||
|
||||
if count.zero?
|
||||
[]
|
||||
else
|
||||
buffer = FFI::MemoryPointer.new(:uint, count)
|
||||
Pdfium.FPDF_GetTrailerEnds(@document_ptr, buffer, count)
|
||||
|
||||
buffer.read_array_of_uint(count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def save(io, flags: Pdfium::FPDF_NO_INCREMENTAL)
|
||||
ensure_not_closed!
|
||||
|
||||
@@ -589,7 +691,74 @@ class Pdfium
|
||||
end
|
||||
end
|
||||
|
||||
class Signature
|
||||
attr_reader :document, :index, :signature_ptr
|
||||
|
||||
def initialize(document, index)
|
||||
@document = document
|
||||
@index = index
|
||||
@signature_ptr = Pdfium.FPDF_GetSignatureObject(document.document_ptr, index)
|
||||
|
||||
raise PdfiumError, "Failed to load signature #{index}, pointer is NULL." if @signature_ptr.null?
|
||||
end
|
||||
|
||||
def byte_range
|
||||
@byte_range ||=
|
||||
begin
|
||||
count = Pdfium.FPDFSignatureObj_GetByteRange(signature_ptr, nil, 0)
|
||||
buffer = FFI::MemoryPointer.new(:int, count)
|
||||
Pdfium.FPDFSignatureObj_GetByteRange(signature_ptr, buffer, count)
|
||||
|
||||
buffer.read_array_of_int(count)
|
||||
end
|
||||
end
|
||||
|
||||
def signed_end
|
||||
@signed_end ||= byte_range.last(2).sum
|
||||
end
|
||||
|
||||
def contents
|
||||
@contents ||= read_bytes(:FPDFSignatureObj_GetContents)
|
||||
end
|
||||
|
||||
def sub_filter
|
||||
@sub_filter ||= read_bytes(:FPDFSignatureObj_GetSubFilter).to_s.delete("\u0000")
|
||||
end
|
||||
|
||||
def time
|
||||
@time ||= read_bytes(:FPDFSignatureObj_GetTime).to_s.delete("\u0000")
|
||||
end
|
||||
|
||||
def reason
|
||||
@reason ||=
|
||||
begin
|
||||
bytes = read_bytes(:FPDFSignatureObj_GetReason)
|
||||
|
||||
bytes&.force_encoding('UTF-16LE')&.encode('UTF-8').to_s.delete("\u0000").presence
|
||||
end
|
||||
end
|
||||
|
||||
def doc_mdp_permission
|
||||
@doc_mdp_permission ||= Pdfium.FPDFSignatureObj_GetDocMDPPermission(signature_ptr)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_bytes(function)
|
||||
length = Pdfium.public_send(function, signature_ptr, nil, 0)
|
||||
|
||||
return if length.zero?
|
||||
|
||||
buffer = FFI::MemoryPointer.new(:char, length)
|
||||
Pdfium.public_send(function, signature_ptr, buffer, length)
|
||||
|
||||
buffer.read_bytes(length)
|
||||
end
|
||||
end
|
||||
|
||||
class Page
|
||||
RECT_KEYS = %i[left top right bottom].freeze
|
||||
|
||||
attr_reader :document, :page_index, :page_ptr
|
||||
|
||||
def initialize(document, page_index)
|
||||
@@ -619,6 +788,34 @@ class Pdfium
|
||||
@height ||= Pdfium.FPDF_GetPageHeightF(@page_ptr)
|
||||
end
|
||||
|
||||
def annotations
|
||||
(0...Pdfium.FPDFPage_GetAnnotCount(page_ptr)).filter_map do |index|
|
||||
annotation = Pdfium.FPDFPage_GetAnnot(page_ptr, index)
|
||||
|
||||
next if annotation.null?
|
||||
|
||||
begin
|
||||
rect = Pdfium::FS_RECTF.new
|
||||
Pdfium.FPDFAnnot_GetRect(annotation, rect)
|
||||
|
||||
[Pdfium.FPDFAnnot_GetSubtype(annotation),
|
||||
*RECT_KEYS.map { |key| rect[key].round(3) }]
|
||||
ensure
|
||||
Pdfium.FPDFPage_CloseAnnot(annotation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def objects
|
||||
(0...Pdfium.FPDFPage_CountObjects(page_ptr)).map do |index|
|
||||
object = Pdfium.FPDFPage_GetObject(page_ptr, index)
|
||||
bounds = Array.new(4) { FFI::MemoryPointer.new(:float) }
|
||||
Pdfium.FPDFPageObj_GetBounds(object, *bounds)
|
||||
|
||||
[Pdfium.FPDFPageObj_GetType(object), *bounds.map { |b| b.read_float.round(3) }]
|
||||
end
|
||||
end
|
||||
|
||||
def rotation
|
||||
@rotation ||= Pdfium.FPDFPage_GetRotation(@page_ptr)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module VerifyPdfSignature
|
||||
COMMON_NAME = 'CN'
|
||||
TIME_FORMAT = '%Y%m%d%H%M%S%z'
|
||||
|
||||
SignatureStruct = Struct.new(:messages, :reason, :signing_time, :common_name, :type)
|
||||
MessageStruct = Struct.new(:text, :status)
|
||||
|
||||
module_function
|
||||
|
||||
def call(io, trusted_certs)
|
||||
Pdfium::Document.open_io(io) do |document|
|
||||
signatures = document.signatures.select { |e| e.byte_range.any?(&:positive?) && e.contents.present? }
|
||||
|
||||
next [] if signatures.blank?
|
||||
|
||||
has_unsigned_changes = unsigned_changes?(document, io)
|
||||
|
||||
signatures.map.with_index do |signature, index|
|
||||
build_signature(signature, io, trusted_certs,
|
||||
has_unsigned_changes && index == signatures.size - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_signature(signature, io, trusted_certs, has_unsigned_changes)
|
||||
pkcs7 = OpenSSL::PKCS7.new(signature.contents)
|
||||
verified = verify_contents(pkcs7, signed_data(io, signature.byte_range), trusted_certs)
|
||||
|
||||
SignatureStruct.new(
|
||||
messages: build_messages(pkcs7, verified, trusted_certs, has_unsigned_changes),
|
||||
reason: signature.reason,
|
||||
signing_time: signing_time(pkcs7, signature),
|
||||
common_name: common_name(pkcs7),
|
||||
type: signature.sub_filter
|
||||
)
|
||||
rescue OpenSSL::PKCS7::PKCS7Error
|
||||
SignatureStruct.new(
|
||||
messages: [MessageStruct.new(text: I18n.t('signature_verification_failed'), status: :error)],
|
||||
reason: signature.reason,
|
||||
signing_time: parse_time(signature.time),
|
||||
type: signature.sub_filter
|
||||
)
|
||||
end
|
||||
|
||||
def build_messages(pkcs7, verified, trusted_certs, has_unsigned_changes)
|
||||
messages =
|
||||
if verified
|
||||
[MessageStruct.new(text: I18n.t('signature_valid'), status: :success),
|
||||
certificate_message(pkcs7, trusted_certs)]
|
||||
else
|
||||
[MessageStruct.new(text: I18n.t('signature_verification_failed'), status: :error)]
|
||||
end
|
||||
|
||||
if has_unsigned_changes
|
||||
messages << MessageStruct.new(text: I18n.t('contains_unsigned_changes_after_the_last_signature'),
|
||||
status: :warning)
|
||||
end
|
||||
|
||||
messages << MessageStruct.new(text: "Certificate chain: #{certificate_chain(pkcs7).join(' -> ')}")
|
||||
end
|
||||
|
||||
def certificate_message(pkcs7, trusted_certs)
|
||||
public_key = signer_certificate(pkcs7)&.public_key&.to_der
|
||||
|
||||
if trusted_certs.any? { |e| e.public_key.to_der == public_key }
|
||||
MessageStruct.new(text: I18n.t('signed_with_trusted_certificate'), status: :success)
|
||||
else
|
||||
MessageStruct.new(text: I18n.t('signed_with_external_certificate'), status: :error)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_contents(pkcs7, signed_data, trusted_certs)
|
||||
return false if digest_algorithms(pkcs7).blank?
|
||||
|
||||
store = OpenSSL::X509::Store.new
|
||||
store.set_default_paths
|
||||
store.purpose = OpenSSL::X509::PURPOSE_SMIME_SIGN
|
||||
store.verify_callback = ->(_success, _context) { true }
|
||||
trusted_certs.each { |cert| store.add_cert(cert) }
|
||||
|
||||
pkcs7.verify(pkcs7.certificates, store, signed_data,
|
||||
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
||||
end
|
||||
|
||||
def digest_algorithms(pkcs7)
|
||||
OpenSSL::ASN1.decode(pkcs7.to_der).value[1].value[0].value[1].value
|
||||
end
|
||||
|
||||
def common_name(pkcs7)
|
||||
cert = signer_certificate(pkcs7)
|
||||
|
||||
return if cert.nil?
|
||||
|
||||
cert.subject.to_a.assoc(COMMON_NAME)&.dig(1)
|
||||
end
|
||||
|
||||
def signer_certificate(pkcs7)
|
||||
info = pkcs7.signers.first
|
||||
|
||||
pkcs7.certificates&.find { |cert| cert.issuer == info.issuer && cert.serial == info.serial }
|
||||
end
|
||||
|
||||
def certificate_chain(pkcs7)
|
||||
signer = signer_certificate(pkcs7)
|
||||
|
||||
return [] if signer.nil?
|
||||
|
||||
certs = [signer]
|
||||
|
||||
while (issuer = pkcs7.certificates.find { |cert| cert.subject == certs.last.issuer })
|
||||
break if certs.include?(issuer)
|
||||
|
||||
certs << issuer
|
||||
end
|
||||
|
||||
certs.map { |cert| cert.subject.to_a.assoc(COMMON_NAME)&.dig(1) }
|
||||
end
|
||||
|
||||
def signing_time(pkcs7, signature)
|
||||
cms_signing_time(pkcs7) || parse_time(signature.time)
|
||||
end
|
||||
|
||||
def cms_signing_time(pkcs7)
|
||||
pkcs7.signers.first&.signed_time
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
def parse_time(value)
|
||||
return if value.blank?
|
||||
|
||||
time = value.delete("'").delete_prefix('D:')
|
||||
offset = time[14..].to_s
|
||||
|
||||
Time.strptime("#{time.first(14)}#{offset.start_with?('+', '-') ? offset : '+0000'}", TIME_FORMAT)
|
||||
end
|
||||
|
||||
def unsigned_changes?(document, io)
|
||||
signed_end = document.signatures.map(&:signed_end).max
|
||||
|
||||
return false if document.trailer_ends.none? { |offset| offset > signed_end }
|
||||
|
||||
io.seek(0)
|
||||
|
||||
Pdfium::Document.open_bytes(io.read(signed_end)) do |signed_document|
|
||||
next false unless signed_document.valid_cross_reference_table?
|
||||
|
||||
serialized_document(signed_document) != serialized_document(document)
|
||||
end
|
||||
end
|
||||
|
||||
def serialized_document(document)
|
||||
pages = (0...document.page_count).map do |index|
|
||||
page = document.get_page(index)
|
||||
|
||||
[page.rotation, page.objects, page.annotations, page.text]
|
||||
end
|
||||
|
||||
[pages, document.bookmarks]
|
||||
end
|
||||
|
||||
def signed_data(io, byte_range)
|
||||
byte_range.each_slice(2).map do |offset, length|
|
||||
io.seek(offset)
|
||||
io.read(length)
|
||||
end.join
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user