Class: PdfOxide::PdfSigner

Inherits:
Object
  • Object
show all
Defined in:
lib/pdf_oxide/pdf_signer.rb

Overview

PAdES B-B / B-T / B-LT / B-LTA digital-signature signer (v0.3.50 #235 + v0.3.51 5-arg shim).

Mirrors ‘fyi.oxide.pdf.PdfSigner`. Routes every sign through the 5-arg shim `pdf_sign_bytes_pades_opts` (the 18-arg legacy entry exists but isn’t exercised here — purego on SysV/AMD64 can’t register it).

Per ‘feedback_extraction_graceful_fallback`: signing is a **security operation** — every non-zero return fails closed.

Defined Under Namespace

Classes: PadesSignOptions

Constant Summary collapse

LEVELS =

PAdES baseline level codes (mirrors Java’s ‘SignatureLevel` enum).

{ b: 0, t: 1, lt: 2, lta: 3 }.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(certificate_handle) ⇒ PdfSigner

Returns a new instance of PdfSigner.

Parameters:

  • certificate_handle (FFI::Pointer)

    PKCS#12 or PEM-loaded credentials handle (opaque pointer from the credentials API).

Raises:



42
43
44
45
46
# File 'lib/pdf_oxide/pdf_signer.rb', line 42

def initialize(certificate_handle)
  raise ::PdfOxide::ArgumentError, 'certificate_handle required' if certificate_handle.nil? || certificate_handle.null?

  @certificate_handle = certificate_handle
end

Class Method Details

.document_has_timestamp?(document_handle) ⇒ Boolean

Returns whether the doc carries a document-scoped /DocTimeStamp.

Returns:

  • (Boolean)

    whether the doc carries a document-scoped /DocTimeStamp.

Raises:



95
96
97
98
99
100
101
102
103
104
# File 'lib/pdf_oxide/pdf_signer.rb', line 95

def self.document_has_timestamp?(document_handle)
  raise ::PdfOxide::ArgumentError, 'document_handle required' if document_handle.nil? || document_handle.null?

  err = ::FFI::MemoryPointer.new(:int32)
  r = Bindings.pdf_document_has_timestamp(document_handle, err)
  code = err.read_int32
  raise SignatureError, "pdf_document_has_timestamp failed (#{code})" if code != 0

  r != 0
end

.pades_level(signature_handle) ⇒ Integer?

Returns the PAdES level of an existing signature handle, or nil if no signatures.

Returns:

  • (Integer, nil)

    the PAdES level of an existing signature handle, or nil if no signatures.

Raises:



83
84
85
86
87
88
89
90
91
92
# File 'lib/pdf_oxide/pdf_signer.rb', line 83

def self.pades_level(signature_handle)
  raise ::PdfOxide::ArgumentError, 'signature_handle required' if signature_handle.nil? || signature_handle.null?

  err = ::FFI::MemoryPointer.new(:int32)
  ordinal = Bindings.pdf_signature_get_pades_level(signature_handle, err)
  code = err.read_int32
  raise SignatureError, "pdf_signature_get_pades_level failed (#{code})" if code != 0

  ordinal
end

.sign(pdf:, certificate_handle:, level:, tsa_url: nil, reason: nil, location: nil) ⇒ String

Static convenience — sign without constructing a Signer instance.

Returns:

  • (String)


77
78
79
# File 'lib/pdf_oxide/pdf_signer.rb', line 77

def self.sign(pdf:, certificate_handle:, level:, tsa_url: nil, reason: nil, location: nil)
  new(certificate_handle).sign(pdf, level: level, tsa_url: tsa_url, reason: reason, location: location)
end

.sign_with_handle(pdf, certificate_handle:, level_code:, tsa_url:, reason:, location:) ⇒ Object

Raises:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/pdf_oxide/pdf_signer.rb', line 107

def self.sign_with_handle(pdf, certificate_handle:, level_code:, tsa_url:, reason:, location:)
  binary = pdf.dup.force_encoding(Encoding::BINARY)
  pdf_buf = ::FFI::MemoryPointer.new(:uint8, binary.bytesize)
  pdf_buf.write_bytes(binary, 0, binary.bytesize)

  # Hold Ruby string buffers in locals so GC doesn't free them while
  # the C call is in flight.
  tsa_buf      = string_ptr(tsa_url)
  reason_buf   = string_ptr(reason)
  location_buf = string_ptr(location)

  opts = PadesSignOptions.new
  opts[:certificate_handle] = certificate_handle
  opts[:certs]              = ::FFI::Pointer::NULL
  opts[:cert_lens]          = ::FFI::Pointer::NULL
  opts[:n_certs]            = 0
  opts[:crls]               = ::FFI::Pointer::NULL
  opts[:crl_lens]           = ::FFI::Pointer::NULL
  opts[:n_crls]             = 0
  opts[:ocsps]              = ::FFI::Pointer::NULL
  opts[:ocsp_lens]          = ::FFI::Pointer::NULL
  opts[:n_ocsps]            = 0
  opts[:tsa_url]            = tsa_buf      || ::FFI::Pointer::NULL
  opts[:reason]             = reason_buf   || ::FFI::Pointer::NULL
  opts[:location]           = location_buf || ::FFI::Pointer::NULL
  opts[:level]              = level_code

  out_len = ::FFI::MemoryPointer.new(:size_t)
  err     = ::FFI::MemoryPointer.new(:int32)
  out_ptr = Bindings.pdf_sign_bytes_pades_opts(pdf_buf, binary.bytesize, opts.to_ptr, out_len, err)
  code = err.read_int32

  raise SignatureError, "pdf_sign_bytes_pades_opts failed (#{code}); security op fails closed" if code != 0
  raise SignatureError, 'pdf_sign_bytes_pades_opts returned null; security op fails closed' if out_ptr.nil? || out_ptr.null?

  len = out_len.read(:size_t)
  signed = out_ptr.read_string(len)
  Bindings.free_bytes(out_ptr) if Bindings.respond_to?(:free_bytes)
  signed.force_encoding(Encoding::BINARY)
end

Instance Method Details

#sign(pdf, level:, tsa_url: nil, reason: nil, location: nil) ⇒ String

Sign a PDF (bytes) at the requested PAdES level.

Parameters:

  • pdf (String)

    raw PDF (BINARY).

  • level (Symbol)

    :b, :t, :lt, or :lta.

  • tsa_url (String, nil) (defaults to: nil)

    RFC 3161 TSA URL (required for ≥ :t).

  • reason (String, nil) (defaults to: nil)
  • location (String, nil) (defaults to: nil)

Returns:

  • (String)

    BINARY-encoded signed PDF bytes.

Raises:



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/pdf_oxide/pdf_signer.rb', line 55

def sign(pdf, level:, tsa_url: nil, reason: nil, location: nil)
  raise ::PdfOxide::ArgumentError, 'pdf cannot be empty' if pdf.nil? || pdf.empty?

  level_code = LEVELS.fetch(level) do
    raise ::PdfOxide::ArgumentError, "level must be one of #{LEVELS.keys.inspect}, got #{level.inspect}"
  end
  if level != :b && (tsa_url.nil? || tsa_url.empty?)
    raise ::PdfOxide::ArgumentError, "PAdES #{level} requires tsa_url"
  end

  self.class.sign_with_handle(
    pdf,
    certificate_handle: @certificate_handle,
    level_code: level_code,
    tsa_url: tsa_url,
    reason: reason,
    location: location
  )
end