cert_sign 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5901a478415ed1ceff961e48525f35899789a409bddad68242dd1a9598b07030
4
+ data.tar.gz: e1636dd9763eae686ce75c1ac33bf3a25fce6cd19875454c5d9644c060e762f4
5
+ SHA512:
6
+ metadata.gz: 12752e48d6707391265f7ce4fcd9c0975b5b23ff3829978a0c4c15630434a9165faab86d7b146ac44a6a20551276dc9e2bd6523652d4e428dafbfd32be7b82b7
7
+ data.tar.gz: e0a201b750dd6922b5a916fa6893f5cc4be77445f5206725c773a1e72870e005aaec520ba8ac91e2c1ab78a89c83cc356aa68a6215c71bd8d38dbe7f0a22ec7e
@@ -0,0 +1,104 @@
1
+ # lib/cert_sign/a1_signer.rb
2
+ require "hexapdf"
3
+
4
+ module CertSign
5
+ class A1Signer
6
+ def initialize(key:, cert:, chain: [])
7
+ @key = key
8
+ @cert = cert
9
+ @chain = chain
10
+ end
11
+
12
+ def self.from_p12(path:, password:)
13
+ p12 = OpenSSL::PKCS12.new(File.binread(path), password)
14
+ new(key: p12.key, cert: p12.certificate, chain: (p12.ca_certs || []))
15
+ end
16
+
17
+ # Agora aceita as keywords :signature_type e :tsa_url (e ignora quaisquer outras)
18
+ # visible: { page:, x_pct:, y_pct:, width_pt:, height_pt:, text: }
19
+ def sign_pdf(input_path:, output_path:, visible:, reason: nil, location: nil, contact: nil,
20
+ signature_type: nil, tsa_url: nil, **_ignored)
21
+ doc = HexaPDF::Document.open(input_path)
22
+
23
+ # AcroForm, campo e widget na página desejada
24
+ form = doc.acro_form(create: true)
25
+
26
+ page_index = (visible[:page].to_i - 1).clamp(0, doc.pages.count - 1)
27
+ rect = CertSign::Coordinates.rect_from_percent(
28
+ doc,
29
+ page_index: page_index,
30
+ x_pct: visible[:x_pct].to_f,
31
+ y_pct: visible[:y_pct].to_f,
32
+ width_pt: visible[:width_pt].to_f,
33
+ height_pt: visible[:height_pt].to_f
34
+ )
35
+
36
+ sig_field = form.create_signature_field("Signature#{Time.now.to_i}")
37
+ widget = sig_field.create_widget(doc.pages[page_index], Rect: rect)
38
+
39
+ # === Carimbo visual da assinatura ===
40
+ w = rect[2] - rect[0]
41
+ h = rect[3] - rect[1]
42
+ pad = 6
43
+
44
+ #cn = @cert.subject.to_a.assoc('CN')&..to_s
45
+ cn = @cert.subject.to_s[/CN=([^\/]+)/, 1]
46
+ title = (visible[:text].presence || "Assinado digitalmente").to_s
47
+ now = Time.now.getlocal.strftime("%d/%m/%Y %H:%M:%S")
48
+
49
+ canvas = widget.create_appearance.canvas
50
+
51
+ # fundo + borda
52
+ canvas.save_graphics_state do
53
+ canvas.line_width(0.8)
54
+ canvas.stroke_color(0.25)
55
+ canvas.fill_color(0.96)
56
+ canvas.rectangle(0, 0, w, h).fill_stroke
57
+ end
58
+
59
+ # título
60
+ y = h - pad - 11
61
+ canvas.fill_color(0)
62
+ canvas.font("Helvetica", variant: :bold, size: 10)
63
+ canvas.text(title, at: [pad, y])
64
+ y -= 14
65
+
66
+ # linhas
67
+ canvas.font("Helvetica", size: 9)
68
+ [
69
+ ("Por: #{cn}" unless cn.empty?),
70
+ "Data: #{now}",
71
+ "Motivo: #{(reason || CertSign.config.default_reason)}",
72
+ ("Local: #{(location || CertSign.config.default_location)}" if location || CertSign.config.default_location)
73
+ ].compact.each do |line|
74
+ break if y < pad + 9
75
+ canvas.text(line, at: [pad, y])
76
+ y -= 12
77
+ end
78
+ # === fim do carimbo ===
79
+
80
+
81
+ # Timestamp handler (opcional)
82
+ ts_handler = nil
83
+ if tsa_url.to_s.strip != ""
84
+ # Em HexaPDF 1.x o helper é exposto via document
85
+ ts_handler = doc.signatures.signing_handler(name: :timestamp, tsa_url: tsa_url) rescue nil
86
+ end
87
+
88
+ # Chamada de assinatura (HexaPDF 1.4+)
89
+ # signature_type pode ser :pades (quando quiser PAdES) ou nil (CMS padrão)
90
+ doc.sign(
91
+ output_path,
92
+ signature: sig_field,
93
+ reason: reason || CertSign.config.default_reason,
94
+ location: location || CertSign.config.default_location,
95
+ contact_info: contact || CertSign.config.default_contact,
96
+ certificate: @cert,
97
+ key: @key,
98
+ certificate_chain: @chain,
99
+ signature_type: (signature_type&.to_sym),
100
+ timestamp_handler: ts_handler
101
+ )
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,62 @@
1
+ # lib/cert_sign/a1_signer.rb
2
+ require "hexapdf"
3
+
4
+ module CertSign
5
+ class A3Signer
6
+ def initialize(key:, cert:, chain: [])
7
+ @key = key
8
+ @cert = cert
9
+ @chain = chain
10
+ end
11
+
12
+ def self.from_p12(path:, password:)
13
+ p12 = OpenSSL::PKCS12.new(File.binread(path), password)
14
+ new(key: p12.key, cert: p12.certificate, chain: (p12.ca_certs || []))
15
+ end
16
+
17
+ # visible: { page:, x_pct:, y_pct:, width_pt:, height_pt:, text: }
18
+ def sign_pdf(input_path:, output_path:, visible:, reason: nil, location: nil, contact: nil, signature_type: nil, tsa_url: nil)
19
+ doc = HexaPDF::Document.open(input_path)
20
+
21
+ # 1) cria (ou reusa) o AcroForm
22
+ form = doc.acro_form(create: true)
23
+
24
+ # 2) cria o campo de assinatura e o widget na página alvo
25
+ page_index = (visible[:page].to_i - 1).clamp(0, doc.pages.count - 1)
26
+ rect = CertSign::Coordinates.rect_from_percent(
27
+ doc,
28
+ page_index: page_index,
29
+ x_pct: visible[:x_pct].to_f,
30
+ y_pct: visible[:y_pct].to_f,
31
+ width_pt: visible[:width_pt].to_f,
32
+ height_pt: visible[:height_pt].to_f
33
+ )
34
+ sig_field = form.create_signature_field("Signature#{Time.now.to_i}")
35
+ widget = sig_field.create_widget(doc.pages[page_index], Rect: rect)
36
+
37
+ # aparencia simples (texto); você pode desenhar logo/imagem aqui
38
+ widget.create_appearance.canvas.
39
+ font("Helvetica", size: 9).
40
+ text((visible[:text] || "").to_s, at: [4, 4])
41
+
42
+ # 3) handler de timestamp (opcional) — estilo 1.4
43
+ ts_handler = nil
44
+ if tsa_url.to_s.present?
45
+ ts_handler = doc.signatures.signing_handler(name: :timestamp, tsa_url: tsa_url)
46
+ end
47
+
48
+ # 4) chama o sign – padrão é CMS (adbe.pkcs7.detached). Para PAdES use signature_type: :pades
49
+ doc.sign(output_path,
50
+ signature: sig_field,
51
+ reason: (reason || CertSign.config.default_reason),
52
+ location: (location || CertSign.config.default_location),
53
+ contact_info: (contact || CertSign.config.default_contact),
54
+ certificate: @cert,
55
+ key: @key,
56
+ certificate_chain: @chain,
57
+ signature_type: (signature_type&.to_sym), # ex.: :pades ou nil p/ CMS
58
+ timestamp_handler: ts_handler
59
+ )
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,22 @@
1
+ module CertSign
2
+ class Config
3
+ attr_accessor :ca_bundle_path, :tsa_url, :default_reason, :default_location, :default_contact
4
+
5
+ def initialize
6
+ @ca_bundle_path = nil
7
+ @tsa_url = nil
8
+ @default_reason = "Assinatura eletrônica"
9
+ @default_location = "Brasil"
10
+ @default_contact = ""
11
+ end
12
+ end
13
+
14
+ def self.config
15
+ @config ||= Config.new
16
+ end
17
+
18
+ def self.configure
19
+ yield config
20
+ end
21
+ end
22
+
@@ -0,0 +1,28 @@
1
+ # Converte % do canvas para pontos PDF (origem: canto inferior esquerdo do PDF)
2
+ module CertSign
3
+ module Coordinates
4
+ module_function
5
+
6
+ # x_pct, y_pct ∈ [0,1] (y_pct medido do TOPO do preview)
7
+ # width_pt / height_pt em pontos (1pt = 1/72 pol)
8
+ def rect_from_percent(doc, page_index:, x_pct:, y_pct:, width_pt:, height_pt:)
9
+ page = doc.pages[page_index]
10
+ box = page.box(:media)
11
+ pdf_w = box.width.to_f
12
+ pdf_h = box.height.to_f
13
+
14
+ llx = (x_pct * pdf_w).round(2)
15
+ lly = ((1.0 - y_pct) * pdf_h - height_pt).round(2)
16
+ urx = (llx + width_pt).round(2)
17
+ ury = (lly + height_pt).round(2)
18
+
19
+ # Mantém dentro da página
20
+ llx = [[0, llx].max, pdf_w - width_pt].min
21
+ lly = [[0, lly].max, pdf_h - height_pt].min
22
+ urx = llx + width_pt
23
+ ury = lly + height_pt
24
+
25
+ [llx, lly, urx, ury]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module CertSign
2
+ VERSION = "1.0.0"
3
+ end
data/lib/cert_sign.rb ADDED
@@ -0,0 +1,11 @@
1
+ # lib/cert_sign.rb
2
+ require_relative "cert_sign/version"
3
+ require_relative "cert_sign/config"
4
+ require_relative "cert_sign/coordinates"
5
+ require_relative "cert_sign/a1_signer"
6
+ require_relative "cert_sign/a3_signer"
7
+
8
+ module CertSign
9
+ # aqui você pode colocar helpers de namespace globais, se quiser
10
+ end
11
+
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cert_sign
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Luiz Cláudio de Castro Figueredo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hexapdf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.41'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.41'
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Assine PDFs em Ruby/Rails usando HexaPDF. Preview no browser, marcação
56
+ do local e assinatura PAdES.
57
+ email:
58
+ - luizfigueredo@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/cert_sign.rb
64
+ - lib/cert_sign/a1_signer.rb
65
+ - lib/cert_sign/a3_signer.rb
66
+ - lib/cert_sign/config.rb
67
+ - lib/cert_sign/coordinates.rb
68
+ - lib/cert_sign/version.rb
69
+ homepage: https://github.com/luizclaudiocfigueredo
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.5.22
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: Assinatura digital PAdES (PDF) com certificado A1/A3 (ICP-Brasil pronto).
92
+ test_files: []