rubrik 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81d2c2fdaaf16157493e2d51d65e7b96695b6c3392456903f312e3809745405c
4
- data.tar.gz: dc1d3640cca46b468417fc15065c621ccc4187f74687fba7431eabac3088a00a
3
+ metadata.gz: ae6274bc80e3730c21dde2bb42803623c024ab0375a2e38880aa14da6c0ba6cc
4
+ data.tar.gz: e1b24c8c6ec21e1b219391044a1e03d29f3d1ea248328f325f4b702417a7fc87
5
5
  SHA512:
6
- metadata.gz: cfa4044ac36e578939f5759b98a1e3f4ff87794dc66f7613f204097f27b0b7640d41353f728c5e5273ae6994310a6175a5df99c7a3685566342df359ae272d8f
7
- data.tar.gz: 917ed93c196bb299da3e726fe09b4e534c8c333471a16a8c3ef672cc3dc2ef91971c6649c4770360df99e4f70c8a0037e563b7510a29b67f1f4764fa10d8d7c5
6
+ metadata.gz: 2568b1b1ab8bf192fea67d035b5409cbe1ed04872fa503240adc9ae51909329fece4ea50479ff3b76a201dad5d1ef582c7cbad77c5d584116f0880eadefd52eb
7
+ data.tar.gz: 001f16bcd75fa30693d9e019bd110d1bf8e8d352a5deda1c130b7d81368f906908ae0cb63e76c033356cb41229282d0910d7e6291dd5bc49640b1b7c57b9a197
data/README.md CHANGED
@@ -4,21 +4,33 @@ Rubrik is a complete and simple digital signature library that implements the PA
4
4
  Signatures) in pure Ruby. It conforms with PKCS#7 and **will be** compatible with Brazil's AD-RB, AD-RT and EU's B-B
5
5
  and B-T profiles.
6
6
 
7
+ - [Rubrik](#rubrik)
8
+ - [Implementation Status](#implementation-status)
9
+ - [PDF Features](#pdf-features)
10
+ - [Signature Profiles](#signature-profiles)
11
+ - [Installation](#installation)
12
+ - [Usage](#usage)
13
+ - [Development](#development)
14
+ - [References:](#references)
15
+ - [Contributing](#contributing)
16
+ - [License](#license)
17
+ - [Code of Conduct](#code-of-conduct)
18
+
7
19
  ## Implementation Status
8
20
 
9
21
  This gem is under development and may be subjected to breaking changes.
10
22
 
11
23
  ### PDF Features
12
- - [x] Modify PDFs with incremental updates (doesn't modify the documents, only append signature appearence)
13
- - [] Signature appearence (stamp)
14
- - [] External (offline) signatures
24
+ - [x] Modify PDFs with incremental updates (doesn't modify the documents, only append signature appearance)
25
+ - [ ] Signature appearance (stamp)
26
+ - [ ] External (offline) signatures
15
27
 
16
28
  ### Signature Profiles
17
29
  - [x] CMS (PKCS#7)
18
- - [] PAdES B-B (conforms with PAdES-E-BES)
19
- - [] PAdES B-T (conforms with PAdES-E-BES)
20
- - [] PAdES AD-RB
21
- - [] PAdES AD-RT
30
+ - [ ] PAdES B-B (conforms with PAdES-E-BES)
31
+ - [ ] PAdES B-T (conforms with PAdES-E-BES)
32
+ - [ ] PAdES AD-RB
33
+ - [ ] PAdES AD-RT
22
34
 
23
35
  ## Installation
24
36
 
@@ -37,7 +49,7 @@ With the gem loaded, run the following to sign an document:
37
49
  ```ruby
38
50
  # The input and output can be of types `File`, `Tempfile` or `StringIO`.
39
51
  input_pdf = File.open("example.pdf", "rb")
40
- output_pdf = File.open("signed_example.pdf", "wb")
52
+ output_pdf = File.open("signed_example.pdf", "wb+") # needs read permission
41
53
 
42
54
  # Load Certificate(s)
43
55
  certificate = File.open("example_cert.pem", "rb")
@@ -63,6 +75,16 @@ After checking out the repo, run `bundle install` to install dependencies. Then,
63
75
 
64
76
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
65
77
 
78
+ ## References:
79
+ 1. PDF References:
80
+ - [ISO 3200-2](https://pdfa.org/sponsored-standards/)
81
+
82
+ 2. Brazil's technical references:
83
+ - [DOC-ICP-15.03](https://www.gov.br/iti/pt-br/assuntos/legislacao/instrucoes-normativas/IN032021_DOC_15.03_assinada.pdf)
84
+ 3. EU's technical references:
85
+ - [ETSI 319 122-1](https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.02.01_60/en_31912201v010201p.pdf)
86
+ - [ETSI 319 122-2](https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.02.01_60/en_31912201v010201p.pdf)
87
+
66
88
  ## Contributing
67
89
 
68
90
  Bug reports and pull requests are welcome on GitHub at https://github.com/tomascco/rubrik. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/tomascco/rubrik/blob/main/CODE_OF_CONDUCT.md).
@@ -9,19 +9,22 @@ module Rubrik
9
9
  extend T::Sig
10
10
  extend self
11
11
 
12
- sig {params(document: Rubrik::Document, io: T.any(File, Tempfile, StringIO)).returns(T.any(File, Tempfile, StringIO))}
12
+ sig {params(document: Rubrik::Document, io: T.any(File, Tempfile, StringIO)).void}
13
13
  def call(document, io:)
14
14
  document.io.rewind
15
- IO.copy_stream(T.unsafe(document.io), T.unsafe(io))
15
+ IO.copy_stream(document.io, io)
16
16
 
17
17
  io << "\n"
18
- new_xref = Array.new
18
+
19
+ new_xref = T.let([], T::Array[T::Hash[Symbol, Integer]])
20
+ new_xref << {id: 0}
19
21
 
20
22
  document.modified_objects.each do |object|
21
23
  integer_id = T.let(object[:id].to_i, Integer)
22
24
  new_xref << {id: integer_id, offset: io.pos}
23
25
 
24
- io << "#{integer_id} 0 obj\n" "#{serialize(object[:value])}\n" "endobj\n\n"
26
+ value = object[:value]
27
+ io << "#{integer_id} 0 obj\n" "#{SerializeObject[value]}\n" "endobj\n\n"
25
28
  end
26
29
 
27
30
  updated_trailer = document.objects.trailer.dup
@@ -31,72 +34,34 @@ module Rubrik
31
34
  new_xref_pos = io.pos
32
35
 
33
36
  new_xref_subsections = new_xref
34
- .sort_by { _1[:id] }
35
- .chunk_while { _1[:id] + 1 == _2[:id] }
37
+ .sort_by { |entry| entry.fetch(:id) }
38
+ .chunk_while { _1.fetch(:id) + 1 == _2[:id] }
36
39
 
37
40
  io << "xref\n"
38
- io << "0 1\n"
39
- io << "0000000000 65535 f\n"
40
41
 
41
42
  new_xref_subsections.each do |subsection|
42
- starting_id = subsection.first[:id]
43
+ starting_id = T.must(subsection.first).fetch(:id)
43
44
  length = subsection.length
44
45
 
45
46
  io << "#{starting_id} #{length}\n"
47
+
48
+ if starting_id.zero?
49
+ io << "0000000000 65535 f\n"
50
+ subsection.shift
51
+ end
52
+
46
53
  subsection.each { |entry| io << "#{format("%010d", entry[:offset])} 00000 n\n" }
47
54
  end
48
55
 
49
56
  io << "trailer\n"
50
- io << "#{serialize(updated_trailer)}\n"
57
+ io << "#{SerializeObject[updated_trailer]}\n"
51
58
  io << "startxref\n"
52
59
  io << "#{new_xref_pos.to_s}\n"
53
60
  io << "%%EOF\n"
54
-
55
- io.rewind
56
- io
57
61
  end
58
62
 
59
63
  private
60
64
 
61
- sig {params(obj: T.untyped).returns(String)}
62
- def serialize(obj)
63
- case obj
64
- when Hash
65
- serialized_objs = obj.flatten.map { |e| serialize(e) }
66
- "<<#{serialized_objs.join(" ")}>>"
67
- when Symbol
68
- "/#{obj}"
69
- when Array
70
- serialized_objs = obj.map { |e| serialize(e) }
71
- "[#{serialized_objs.join(" ")}]"
72
- when PDF::Reader::Reference
73
- "#{obj.id} #{obj.gen} R"
74
- when String
75
- "(#{obj})"
76
- when TrueClass
77
- "true"
78
- when FalseClass
79
- "false"
80
- when Document::CONTENTS_PLACEHOLDER
81
- "<#{"0" * Document::SIGNATURE_SIZE}>"
82
- when Document::BYTE_RANGE_PLACEHOLDER
83
- "[0 0000000000 0000000000 0000000000]"
84
- when Numeric
85
- obj.to_s
86
- when NilClass
87
- "null"
88
- when PDF::Reader::Stream
89
- <<~OBJECT.chomp
90
- #{serialize(obj.hash)}
91
- stream
92
- #{obj.data}
93
- endstream
94
- OBJECT
95
- else
96
- raise NotImplementedError.new("Don't know how to serialize #{obj}")
97
- end
98
- end
99
-
100
65
  sig {params(document: Rubrik::Document).returns(Integer)}
101
66
  def last_xref_pos(document)
102
67
  PDF::Reader::Buffer.new(document.io, seek: 0).find_first_xref_offset
@@ -0,0 +1,53 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rubrik
5
+ class Document
6
+ module SerializeObject
7
+ include Kernel
8
+ extend T::Sig
9
+ extend self
10
+
11
+ sig {params(obj: T.untyped).returns(String)}
12
+ def [](obj)
13
+ case obj
14
+ when Hash
15
+ serialized_objs = obj.flatten.map { |e| SerializeObject[e] }
16
+ "<<#{serialized_objs.join(" ")}>>"
17
+ when Symbol
18
+ "/#{obj}"
19
+ when Array
20
+ serialized_objs = obj.map { |e| SerializeObject[e] }
21
+ "[#{serialized_objs.join(" ")}]"
22
+ when PDF::Reader::Reference
23
+ "#{obj.id} #{obj.gen} R"
24
+ when String
25
+ "(#{obj})"
26
+ when TrueClass
27
+ "true"
28
+ when FalseClass
29
+ "false"
30
+ when Document::CONTENTS_PLACEHOLDER
31
+ "<#{"0" * Document::SIGNATURE_SIZE}>"
32
+ when Document::BYTE_RANGE_PLACEHOLDER
33
+ "[0 0000000000 0000000000 0000000000]"
34
+ when Float, Integer
35
+ obj.to_s
36
+ when NilClass
37
+ "null"
38
+ when PDF::Reader::Stream
39
+ <<~OBJECT.chomp
40
+ #{SerializeObject[obj.hash]}
41
+ stream
42
+ #{obj.data}
43
+ endstream
44
+ OBJECT
45
+ else
46
+ raise "Don't know how to serialize #{obj}"
47
+ end
48
+ end
49
+
50
+ alias call []
51
+ end
52
+ end
53
+ end
@@ -12,19 +12,18 @@ module Rubrik
12
12
  SIGNATURE_SIZE = 8_192
13
13
 
14
14
  sig {returns(T.any(File, Tempfile, StringIO))}
15
- attr_reader :io
15
+ attr_accessor :io
16
16
 
17
17
  sig {returns(PDF::Reader::ObjectHash)}
18
- attr_reader :objects
18
+ attr_accessor :objects
19
19
 
20
20
  sig {returns(T::Array[{id: PDF::Reader::Reference, value: T.untyped}])}
21
- attr_reader :modified_objects
22
-
23
- sig {returns(PDF::Reader::Reference)}
24
- attr_reader :interactive_form_id
21
+ attr_accessor :modified_objects
25
22
 
26
23
  sig {returns(Integer)}
27
- attr_reader :last_object_id
24
+ attr_accessor :last_object_id
25
+
26
+ private :io=, :objects=, :modified_objects=, :last_object_id=
28
27
 
29
28
  sig {params(input: T.any(File, Tempfile, StringIO)).void}
30
29
  def initialize(input)
@@ -36,9 +35,11 @@ module Rubrik
36
35
  fetch_or_create_interactive_form!
37
36
  end
38
37
 
39
- sig {void}
38
+ sig {returns(PDF::Reader::Reference)}
39
+ # Returns the reference of the Signature Value dictionary.
40
40
  def add_signature_field
41
- # create signature value dictionary
41
+ # To add an signature to the PDF, we need the following structure
42
+ # Interactive Form -> Signature Field -> Signature Value
42
43
  signature_value_id = assign_new_object_id!
43
44
  modified_objects << {
44
45
  id: signature_value_id,
@@ -63,7 +64,7 @@ module Rubrik
63
64
  V: signature_value_id,
64
65
  Type: :Annot,
65
66
  Subtype: :Widget,
66
- Rect: [20, 20, 120, 120],
67
+ Rect: [0, 0, 0, 0],
67
68
  F: 4,
68
69
  P: first_page_reference
69
70
  }
@@ -75,6 +76,8 @@ module Rubrik
75
76
  modified_objects << {id: first_page_reference, value: modified_page}
76
77
 
77
78
  (interactive_form[:Fields] ||= []) << signature_field_id
79
+
80
+ signature_value_id
78
81
  end
79
82
 
80
83
  private
@@ -87,7 +90,7 @@ module Rubrik
87
90
  sig {void}
88
91
  def fetch_or_create_interactive_form!
89
92
  root_ref = objects.trailer[:Root]
90
- root = T.let(objects.fetch(root_ref), Hash)
93
+ root = T.let(objects.fetch(root_ref), T::Hash[Symbol, T.untyped])
91
94
 
92
95
  if root.key?(:AcroForm)
93
96
  form_id = root[:AcroForm]
@@ -111,17 +114,5 @@ module Rubrik
111
114
  def assign_new_object_id!
112
115
  PDF::Reader::Reference.new(self.last_object_id += 1, 0)
113
116
  end
114
-
115
- sig {params(io: T.any(File, Tempfile, StringIO)).returns(T.any(File, Tempfile, StringIO))}
116
- attr_writer :io
117
-
118
- sig {params(objects: PDF::Reader::ObjectHash).returns(PDF::Reader::ObjectHash)}
119
- attr_writer :objects
120
-
121
- sig {params(modified_objects: T::Array[{id: PDF::Reader::Reference, value: T.untyped}]).returns(T::Array[{id: PDF::Reader::Reference, value: T.untyped}])}
122
- attr_writer :modified_objects
123
-
124
- sig {params(last_object_id: Integer).returns(Integer)}
125
- attr_writer :last_object_id
126
117
  end
127
118
  end
@@ -15,7 +15,7 @@ module Rubrik
15
15
  private_key: OpenSSL::PKey::RSA,
16
16
  public_key: OpenSSL::X509::Certificate,
17
17
  certificate_chain: T::Array[OpenSSL::X509::Certificate])
18
- .returns(T.any(File, StringIO, Tempfile))}
18
+ .void}
19
19
 
20
20
  FIRST_OFFSET = 0
21
21
 
@@ -29,8 +29,7 @@ module Rubrik
29
29
  io.gets("<")
30
30
 
31
31
  first_length = io.pos - 1
32
- # we need to double the SIGNATURE_SIZE because the hex encoding double the data size
33
- # we also need to sum +2 to account for "<" and ">" of the hex string
32
+ # We need to sum +2 to account for "<" and ">" of the hex string
34
33
  second_offset = first_length + Document::SIGNATURE_SIZE + 2
35
34
  second_length = io.size - second_offset
36
35
 
data/lib/rubrik/sign.rb CHANGED
@@ -13,15 +13,15 @@ module Rubrik
13
13
  certificate_chain: T::Array[OpenSSL::X509::Certificate])
14
14
  .void}
15
15
  def self.call(input, output, private_key:, public_key:, certificate_chain: [])
16
+ input.binmode
17
+ output.reopen(T.unsafe(output), "wb+") if !output.is_a?(StringIO)
18
+
16
19
  document = Rubrik::Document.new(input)
17
20
 
18
- document.add_signature_field
21
+ signature_value_ref = document.add_signature_field
19
22
 
20
23
  Document::Increment.call(document, io: output)
21
24
 
22
- signature_value = T.must(document.modified_objects.find { _1.dig(:value, :Type) == :Sig })
23
-
24
- signature_value_ref = T.let(signature_value[:id], PDF::Reader::Reference)
25
25
  FillSignature.call(output, signature_value_ref:, private_key:, public_key:, certificate_chain:)
26
26
  end
27
27
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubrik
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/rubrik.rb CHANGED
@@ -1,12 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "bundler"
5
- Bundler.setup(:default)
6
-
7
4
  require "sorbet-runtime"
8
5
  require "pdf-reader"
9
- require "irb"
10
6
 
11
7
  module Rubrik
12
8
  class Error < StandardError; end
@@ -14,5 +10,6 @@ end
14
10
 
15
11
  require_relative "rubrik/document"
16
12
  require_relative "rubrik/document/increment"
13
+ require_relative "rubrik/document/serialize_object"
17
14
  require_relative "rubrik/fill_signature"
18
15
  require_relative "rubrik/sign"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubrik
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tomás Coêlho
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-16 00:00:00.000000000 Z
11
+ date: 2023-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pdf-reader
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: openssl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.1
41
55
  description: Sign PDFs digitally in pure Ruby
42
56
  email:
43
57
  - tomascoelho6@gmail.com
@@ -50,6 +64,7 @@ files:
50
64
  - lib/rubrik.rb
51
65
  - lib/rubrik/document.rb
52
66
  - lib/rubrik/document/increment.rb
67
+ - lib/rubrik/document/serialize_object.rb
53
68
  - lib/rubrik/fill_signature.rb
54
69
  - lib/rubrik/sign.rb
55
70
  - lib/rubrik/version.rb
@@ -59,7 +74,7 @@ licenses:
59
74
  metadata:
60
75
  homepage_uri: https://github.com/tomascco/rubrik
61
76
  source_code_uri: https://github.com/tomascco/rubrik
62
- changelog_uri: https://github.com/tomascco/rubrik/blob/main/CHANGELOG.md
77
+ changelog_uri: https://github.com/tomascco/rubrik/releases
63
78
  post_install_message:
64
79
  rdoc_options: []
65
80
  require_paths: