Module: Pdfrb::DigitalSignature::Verification

Defined in:
lib/pdfrb/digital_signature/verification.rb

Class Method Summary collapse

Class Method Details

.der_prefix(bytes) ⇒ Object

The DER structure starting at bytes (a SEQUENCE), exactly as long as its own declared length - any placeholder padding after it is dropped. Non-SEQUENCE input passes through.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/pdfrb/digital_signature/verification.rb', line 109

def der_prefix(bytes)
  return bytes if bytes.bytesize < 2 || bytes.getbyte(0) != 0x30

  length = bytes.getbyte(1)
  offset = 2
  if length.anybits?(0x80)
    count = length & 0x7F
    return bytes if count.zero? || bytes.bytesize < 2 + count

    length = 0
    count.times do |i|
      length = (length << 8) | bytes.getbyte(2 + i)
    end
    offset = 2 + count
  end
  bytes.byteslice(0, offset + length)
end

.find_signature(pdf_bytes, start_offset) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/pdfrb/digital_signature/verification.rb', line 26

def find_signature(pdf_bytes, start_offset)
  contents_marker = "/Contents <"
  ck_start = pdf_bytes.index(contents_marker, start_offset)
  return nil unless ck_start

  hex_start = ck_start + contents_marker.bytesize
  hex_end = pdf_bytes.index(">", hex_start)
  return nil unless hex_end

  br_start = pdf_bytes.rindex("/ByteRange [", ck_start)
  return nil unless br_start

  br_str_start = br_start + "/ByteRange [".bytesize
  br_end = pdf_bytes.index("]", br_str_start)
  return nil unless br_end

  br_str = pdf_bytes[br_str_start...br_end]
  byte_range = br_str.split.map(&:to_i)

  {
    byte_range: byte_range,
    contents_hex_start: hex_start,
    contents_hex_end: hex_end,
    contents_end: hex_end,
  }
end

.verify(pdf_bytes, trusted_certs: []) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/pdfrb/digital_signature/verification.rb', line 11

def verify(pdf_bytes, trusted_certs: [])
  results = []
  offset = 0

  loop do
    sig_info = find_signature(pdf_bytes, offset)
    break unless sig_info

    results << verify_signature(pdf_bytes, sig_info, trusted_certs)
    offset = sig_info[:contents_end] + 1
  end

  results
end

.verify_pkcs7(der, signed_data, trusted_certs) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/pdfrb/digital_signature/verification.rb', line 81

def verify_pkcs7(der, signed_data, trusted_certs)
  pkcs7 = OpenSSL::PKCS7.new(der)

  store = OpenSSL::X509::Store.new
  trusted_certs.each { |c| store.add_cert(c) }

  valid = pkcs7.verify(nil, store, signed_data,
                       OpenSSL::PKCS7::DETACHED |
                       OpenSSL::PKCS7::BINARY)

  VerificationResult.new(
    signer: pkcs7.signers.first&.issuer&.to_s,
    valid?: valid,
    byte_range_ok?: true,
    cert_chain: pkcs7.certificates || [],
    trusted?: !trusted_certs.empty? && valid,
    error: valid ? nil : "signature verification failed",
  )
rescue OpenSSL::PKCS7::PKCS7Error, ArgumentError => e
  VerificationResult.new(valid?: false,
                         byte_range_ok?: true,
                         cert_chain: [],
                         error: e.message)
end

.verify_signature(pdf_bytes, sig_info, trusted_certs) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/pdfrb/digital_signature/verification.rb', line 53

def verify_signature(pdf_bytes, sig_info, trusted_certs)
  byte_range = sig_info[:byte_range]

  if byte_range.nil? || byte_range.length != 4
    return VerificationResult.new(valid?: false,
                                  byte_range_ok?: false,
                                  cert_chain: [],
                                  error: "invalid ByteRange")
  end

  _start1, len1, start2, len2 = byte_range
  part1 = pdf_bytes.bytes[0, len1] || []
  part2 = pdf_bytes.bytes[start2, len2] || []
  signed_data = part1.pack("C*") + part2.pack("C*")

  hex_start = sig_info[:contents_hex_start]
  hex_end = sig_info[:contents_hex_end]
  contents_hex = pdf_bytes[hex_start...hex_end]

  # The /Contents placeholder is zero-padded to its reserved
  # size; DER is self-delimiting, so der_prefix reads the
  # declared length instead of guessing where the signature
  # ends (blindly trimming trailing zeros also eats legitimate
  # 0x00 final bytes and can split a hex byte).
  verify_pkcs7(der_prefix([contents_hex].pack("H*")),
               signed_data, trusted_certs)
end