Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verify multiple signature #65

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 53 additions & 32 deletions lib/origami/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,40 @@ class PDF
# _verify_cb_: block called when encountering a certificate that cannot be verified.
# Passed argument in the OpenSSL::X509::StoreContext.
#
# Added a new method to verify all signatures in a signed pdf
def verify(trusted_certs: [],
use_system_store: false,
allow_self_signed: false,
&verify_cb)

digsig = self.signature
digsig = digsig.cast_to(Signature::DigitalSignature) unless digsig.is_a?(Signature::DigitalSignature)
signatures.each_with_index do |digsig, index|
digsig = digsig.cast_to(Signature::DigitalSignature) unless digsig.is_a?(Signature::DigitalSignature)

signature = digsig.signature_data
chain = digsig.certificate_chain
subfilter = digsig.SubFilter.value
signature = digsig.signature_data
chain = digsig.certificate_chain
subfilter = digsig.SubFilter.value

store = OpenSSL::X509::Store.new
store.set_default_paths if use_system_store
trusted_certs.each { |ca| store.add_cert(ca) }
store = OpenSSL::X509::Store.new
store.set_default_paths if use_system_store
trusted_certs.each {|ca| store.add_cert(ca)}

store.verify_callback = -> (success, ctx) {
return true if success
store.verify_callback = -> (success, ctx) {
return true if success

error = ctx.error
is_self_signed = (error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
error == OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN)
error = ctx.error
is_self_signed = (error == OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
error == OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN)

return true if is_self_signed && allow_self_signed && verify_cb.nil?
return true if is_self_signed && allow_self_signed && verify_cb.nil?

verify_cb.call(ctx) unless verify_cb.nil?
}
verify_cb.call(ctx) unless verify_cb.nil?
}

data = extract_signed_data(digsig)
Signature.verify(subfilter.to_s, data, signature, store, chain)
data = index == signatures.length - 1 ? extract_signed_data(digsig, last_sig: true) : extract_signed_data(digsig)
return false unless Signature.verify(subfilter.to_s, data, signature, store, chain)
end

true
end

#
Expand Down Expand Up @@ -306,33 +310,50 @@ def signature
raise SignatureError, "Cannot find digital signature"
end


def signatures
raise SignatureError, "Not a signed document" unless self.signed?

dig_sigs = []
self.each_field do |field|
dig_sigs << field.V if field.FT == :Sig && field.V.is_a?(Dictionary)
end.compact

return dig_sigs if dig_sigs.count > 0
raise SignatureError, "Cannot find digital signature"
end

private

#
# Verifies the ByteRange field of a digital signature and returned the signed data.
# Only check for valid ByteRange when it is the last signature
#
def extract_signed_data(digsig)
def extract_signed_data(digsig, last_sig: false)
# Computes the boundaries of the Contents field.
start_sig = digsig[:Contents].file_offset

stream = StringScanner.new(self.original_data)
stream.pos = digsig[:Contents].file_offset
Object.typeof(stream).parse(stream)
end_sig = stream.pos
stream.terminate

r1, r2 = digsig.ranges
if r1.begin != 0 or
r2.end != self.original_data.size or
r1.end != start_sig or
r2.begin != end_sig

raise SignatureError, "Invalid signature byte range"
if last_sig
start_sig = digsig[:Contents].file_offset

stream = StringScanner.new(self.original_data)
stream.pos = digsig[:Contents].file_offset
Object.typeof(stream).parse(stream)
end_sig = stream.pos
stream.terminate

if r1.begin != 0 or
r2.end != self.original_data.size or
r1.end != start_sig or
r2.begin != end_sig

raise SignatureError, "Invalid signature byte range"
end
end

self.original_data[r1] + self.original_data[r2]
end

end

class Perms < Dictionary
Expand Down
55 changes: 47 additions & 8 deletions test/test_pdf_sign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,7 @@ def setup_document_with_annotation
def sign_document_with_method(method)
document, annotation = setup_document_with_annotation

document.sign(@cert, @key,
method: method,
annotation: annotation,
issuer: "Guillaume Delugré",
location: "France",
contact: "origami@localhost",
reason: "Example"
)
sign_document(annotation, document, method)

assert document.frozen?
assert document.signed?
Expand All @@ -83,6 +76,27 @@ def sign_document_with_method(method)
assert result
end

def sign_document_twice_with_method(method)
document, annotation = setup_document_with_annotation

2.times do
sign_document(annotation, document, method)
end

assert document.frozen?
assert document.signed?

output = StringIO.new
document.save(output)

document = PDF.read(output.reopen(output.string,'r'), verbosity: Parser::VERBOSE_QUIET)

refute document.verify
assert document.verify(allow_self_signed: true)
assert document.verify(trusted_certs: [@cert])
refute document.verify(trusted_certs: [@other_cert])
end

def test_sign_pkcs7_sha1
sign_document_with_method(Signature::PKCS7_SHA1)
end
Expand All @@ -94,4 +108,29 @@ def test_sign_pkcs7_detached
def test_sign_x509_sha1
sign_document_with_method(Signature::PKCS1_RSA_SHA1)
end

def test_sign_pkcs7_sha1_twice
sign_document_twice_with_method(Signature::PKCS7_SHA1)
end

def test_sign_pkcs7_detached_twice
sign_document_twice_with_method(Signature::PKCS7_DETACHED)
end

def test_sign_x509_sha1_twice
sign_document_twice_with_method(Signature::PKCS1_RSA_SHA1)
end

private

def sign_document(annotation, document, method)
document.sign(@cert, @key,
method: method,
annotation: annotation,
issuer: "Guillaume Delugré",
location: "France",
contact: "origami@localhost",
reason: "Example"
)
end
end