rubrik 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|