rubrik 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +24 -2
- data/lib/rubrik/document/increment.rb +17 -52
- data/lib/rubrik/document/serialize_object.rb +53 -0
- data/lib/rubrik/document.rb +14 -23
- data/lib/rubrik/fill_signature.rb +2 -3
- data/lib/rubrik/sign.rb +4 -4
- data/lib/rubrik/version.rb +1 -1
- data/lib/rubrik.rb +1 -1
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9faab13628886a83554fb2dd75b71d7315701da037377bb70d7aba3e9769ffe
|
4
|
+
data.tar.gz: 90fbf56ed6afbd1ab7b54ad4290800ad079fd4b600eb4334f2974a248b2631c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3a83c73ca910d0ce84298c65d7f704ed93ccd3104bb44f1d70331b82bdcdecae1355fb2b5a0b8177a9954b3c39394153cbb521cf67ec4d4a677fc98684c90f5e
|
7
|
+
data.tar.gz: 0da9fd89c054a40db22642cfb4b390be64fb49c12b42f449d901c898b86bcd10d4f8e72892ecfc59618aca1678f115165209a41497315c3b2f1cde8d29882194
|
data/README.md
CHANGED
@@ -4,13 +4,25 @@ 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
|
13
|
-
- [ ] Signature
|
24
|
+
- [x] Modify PDFs with incremental updates (doesn't modify the documents, only append signature appearance)
|
25
|
+
- [ ] Signature appearance (stamp)
|
14
26
|
- [ ] External (offline) signatures
|
15
27
|
|
16
28
|
### Signature Profiles
|
@@ -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)).
|
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(
|
15
|
+
IO.copy_stream(document.io, io)
|
16
16
|
|
17
17
|
io << "\n"
|
18
|
-
|
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
|
-
|
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 {
|
35
|
-
.chunk_while {
|
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
|
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 << "#{
|
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
|
data/lib/rubrik/document.rb
CHANGED
@@ -12,19 +12,18 @@ module Rubrik
|
|
12
12
|
SIGNATURE_SIZE = 8_192
|
13
13
|
|
14
14
|
sig {returns(T.any(File, Tempfile, StringIO))}
|
15
|
-
|
15
|
+
attr_accessor :io
|
16
16
|
|
17
17
|
sig {returns(PDF::Reader::ObjectHash)}
|
18
|
-
|
18
|
+
attr_accessor :objects
|
19
19
|
|
20
20
|
sig {returns(T::Array[{id: PDF::Reader::Reference, value: T.untyped}])}
|
21
|
-
|
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
|
-
|
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 {
|
38
|
+
sig {returns(PDF::Reader::Reference)}
|
39
|
+
# Returns the reference of the Signature Value dictionary.
|
40
40
|
def add_signature_field
|
41
|
-
#
|
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: [
|
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
|
-
.
|
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
|
-
#
|
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
|
data/lib/rubrik/version.rb
CHANGED
data/lib/rubrik.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
|
4
4
|
require "sorbet-runtime"
|
5
5
|
require "pdf-reader"
|
6
|
-
require "irb"
|
7
6
|
|
8
7
|
module Rubrik
|
9
8
|
class Error < StandardError; end
|
@@ -11,5 +10,6 @@ end
|
|
11
10
|
|
12
11
|
require_relative "rubrik/document"
|
13
12
|
require_relative "rubrik/document/increment"
|
13
|
+
require_relative "rubrik/document/serialize_object"
|
14
14
|
require_relative "rubrik/fill_signature"
|
15
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.
|
4
|
+
version: 0.2.1
|
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-
|
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/
|
77
|
+
changelog_uri: https://github.com/tomascco/rubrik/releases
|
63
78
|
post_install_message:
|
64
79
|
rdoc_options: []
|
65
80
|
require_paths:
|
@@ -75,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
90
|
- !ruby/object:Gem::Version
|
76
91
|
version: '0'
|
77
92
|
requirements: []
|
78
|
-
rubygems_version: 3.4.
|
93
|
+
rubygems_version: 3.4.20
|
79
94
|
signing_key:
|
80
95
|
specification_version: 4
|
81
96
|
summary: Sign PDFs digitally in pure Ruby
|