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 +7 -0
- data/lib/cert_sign/a1_signer.rb +104 -0
- data/lib/cert_sign/a3_signer.rb +62 -0
- data/lib/cert_sign/config.rb +22 -0
- data/lib/cert_sign/coordinates.rb +28 -0
- data/lib/cert_sign/version.rb +3 -0
- data/lib/cert_sign.rb +11 -0
- metadata +92 -0
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
|
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: []
|