lib/origami/signature.rb in origami-2.0.0 vs lib/origami/signature.rb in origami-2.0.1

- old
+ new

@@ -16,16 +16,11 @@ You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see <http://www.gnu.org/licenses/>. =end -begin - require 'openssl' if Origami::OPTIONS[:use_openssl] -rescue LoadError - Origami::OPTIONS[:use_openssl] = false -end - +require 'openssl' require 'digest/sha1' module Origami class PDF @@ -36,53 +31,34 @@ # Verify a document signature. # _:trusted_certs_: an array of trusted X509 certificates. # If no argument is passed, embedded certificates are treated as trusted. # def verify(trusted_certs: []) - unless Origami::OPTIONS[:use_openssl] - fail "OpenSSL is not present or has been disabled." - end - digsig = self.signature + digsig = digsig.cast_to(Signature::DigitalSignature) unless digsig.is_a?(Signature::DigitalSignature) unless digsig[:Contents].is_a?(String) raise SignatureError, "Invalid digital signature contents" end store = OpenSSL::X509::Store.new - trusted_certs.each do |ca| store.add_cert(ca) end + trusted_certs.each { |ca| store.add_cert(ca) } flags = 0 flags |= OpenSSL::PKCS7::NOVERIFY if trusted_certs.empty? - stream = StringScanner.new(self.original_data) - stream.pos = digsig[:Contents].file_offset - Object.typeof(stream).parse(stream) - endofsig_offset = stream.pos - stream.terminate + data = extract_signed_data(digsig) + signature = digsig[:Contents] + subfilter = digsig.SubFilter.value - s1,l1,s2,l2 = digsig.ByteRange - if s1.value != 0 or - (s2.value + l2.value) != self.original_data.size or - (s1.value + l1.value) != digsig[:Contents].file_offset or - s2.value != endofsig_offset + case subfilter + when Signature::DigitalSignature::PKCS7_DETACHED + Signature.verify_pkcs7_detached_signature(data, signature, store, flags) - raise SignatureError, "Invalid signature byte range" - end - data = self.original_data[s1,l1] + self.original_data[s2,l2] + when Signature::DigitalSignature::PKCS7_SHA1 + Signature.verify_pkcs7_sha1_signature(data, signature, store, flags) - case digsig.SubFilter.value.to_s - when 'adbe.pkcs7.detached' - flags |= OpenSSL::PKCS7::DETACHED - p7 = OpenSSL::PKCS7.new(digsig[:Contents].value) - raise SignatureError, "Not a PKCS7 detached signature" unless p7.detached? - p7.verify([], store, data, flags) - - when 'adbe.pkcs7.sha1' - p7 = OpenSSL::PKCS7.new(digsig[:Contents].value) - p7.verify([], store, nil, flags) and p7.data == Digest::SHA1.digest(data) - else raise NotImplementedError, "Unsupported method #{digsig.SubFilter}" end end @@ -105,14 +81,10 @@ issuer: nil, location: nil, contact: nil, reason: nil) - unless Origami::OPTIONS[:use_openssl] - fail "OpenSSL is not present or has been disabled." - end - unless certificate.is_a?(OpenSSL::X509::Certificate) raise TypeError, "A OpenSSL::X509::Certificate object must be passed." end unless key.is_a?(OpenSSL::PKey::RSA) @@ -138,22 +110,22 @@ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY ).to_der.size end when 'adbe.pkcs7.sha1' - signfield_size = -> (crt, pkey, certs) do + signfield_size = -> (crt, pkey, certs) do OpenSSL::PKCS7.sign( crt, pkey, Digest::SHA1.digest(''), certs, OpenSSL::PKCS7::BINARY ).to_der.size end when 'adbe.x509.rsa_sha1' - signfield_size = -> (crt, pkey, certs) do + signfield_size = -> (_crt, pkey, _certs) do pkey.private_encrypt( Digest::SHA1.digest('') ).size end raise NotImplementedError, "Unsupported method #{method.inspect}" @@ -170,11 +142,11 @@ end annotation.V = digsig add_fields(annotation) self.Catalog.AcroForm.SigFlags = - InteractiveForm::SigFlags::SIGNATURESEXIST | InteractiveForm::SigFlags::APPENDONLY + InteractiveForm::SigFlags::SIGNATURES_EXIST | InteractiveForm::SigFlags::APPEND_ONLY digsig.Type = :Sig #:nodoc: digsig.Contents = HexaString.new("\x00" * signfield_size[certificate, key, ca]) #:nodoc: digsig.Filter = :"Adobe.PPKLite" #:nodoc: digsig.SubFilter = Name.new(method) #:nodoc: @@ -261,25 +233,22 @@ # Returns whether the document contains a digital signature. # def signed? begin self.Catalog.AcroForm.is_a?(Dictionary) and - self.Catalog.AcroForm.has_key?(:SigFlags) and - (self.Catalog.AcroForm.SigFlags & InteractiveForm::SigFlags::SIGNATURESEXIST != 0) + self.Catalog.AcroForm.SigFlags.is_a?(Integer) and + (self.Catalog.AcroForm.SigFlags & InteractiveForm::SigFlags::SIGNATURES_EXIST != 0) rescue InvalidReferenceError false end end # # Enable the document Usage Rights. # _rights_:: list of rights defined in UsageRights::Rights # def enable_usage_rights(cert, pkey, *rights) - unless Origami::OPTIONS[:use_openssl] - fail "OpenSSL is not present or has been disabled." - end signfield_size = -> (crt, key, ca) do OpenSSL::PKCS7.sign( crt, key, @@ -299,11 +268,11 @@ # Forge digital signature dictionary # digsig = Signature::DigitalSignature.new.set_indirect(true) self.Catalog.AcroForm ||= InteractiveForm.new - #self.Catalog.AcroForm.SigFlags = InteractiveForm::SigFlags::APPENDONLY + #self.Catalog.AcroForm.SigFlags = InteractiveForm::SigFlags::APPEND_ONLY digsig.Type = :Sig #:nodoc: digsig.Contents = HexaString.new("\x00" * signfield_size[certificate, key, []]) #:nodoc: digsig.Filter = :"Adobe.PPKLite" #:nodoc: digsig.Name = "ARE Acrobat Product v8.0 P23 0002337" #:nodoc: @@ -388,10 +357,38 @@ return field.V if field.FT == :Sig and field.V.is_a?(Dictionary) end raise SignatureError, "Cannot find digital signature" end + + private + + # + # Verifies the ByteRange field of a digital signature and returned the signed data. + # + def extract_signed_data(digsig) + # 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" + end + + self.original_data[r1] + self.original_data[r2] + end + end class Perms < Dictionary include StandardObject @@ -400,10 +397,25 @@ field :UR3, :Type => Dictionary, :Version => "1.6" end module Signature + # Verifies a PKCS7 detached signature. + def self.verify_pkcs7_detached_signature(data, signature, store, flags) + pkcs7 = OpenSSL::PKCS7.new(signature) + raise SignatureError, "Not a PKCS7 detached signature" unless pkcs7.detached? + + flags |= OpenSSL::PKCS7::DETACHED + pkcs7.verify([], store, data, flags) + end + + # Verifies a PKCS7-SHA1 signature. + def self.verify_pkcs7_sha1_signature(data, signature, store, flags) + pkcs7 = OpenSSL::PKCS7.new(signature) + pkcs7.verify([], store, nil, flags) and pkcs7.data == Digest::SHA1.digest(data) + end + # # Class representing a signature which can be embedded in DigitalSignature dictionary. # It must be a direct object. # class Reference < Dictionary @@ -492,10 +504,14 @@ # Class representing a digital signature. # class DigitalSignature < Dictionary include StandardObject + PKCS1_RSA_SHA1 = :"adbe.x509.rsa_sha1" + PKCS7_SHA1 = :"adbe.pkcs7.sha1" + PKCS7_DETACHED = :"adbe.pkcs7.detached" + field :Type, :Type => Name, :Default => :Sig field :Filter, :Type => Name, :Default => :"Adobe.PPKLite", :Required => true field :SubFilter, :Type => Name field :Contents, :Type => String, :Required => true field :Cert, :Type => [ String, Array.of(String) ] @@ -533,9 +549,21 @@ end content << tab * (indent - 1) << TOKENS.last output(content) + end + + def ranges + byte_range = self.ByteRange + + unless byte_range.is_a?(Array) and byte_range.length == 4 and byte_range.all? {|i| i.is_a?(Integer) } + raise SignatureError, "Invalid ByteRange field value" + end + + byte_range.map(&:to_i).each_slice(2).map do |start, length| + (start...start + length) + end end def signature_offset #:nodoc: indent, tab = 1, "\t" content = "#{no} #{generation} obj" + EOL + TOKENS.first + EOL