add ability to use timestamp server
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TimestampServerController < ApplicationController
|
||||||
|
before_action :build_encrypted_config
|
||||||
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
|
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||||
|
|
||||||
|
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def test_timeserver_url(url)
|
||||||
|
pdf = HexaPDF::Document.new
|
||||||
|
pdf.pages.add
|
||||||
|
|
||||||
|
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||||
|
|
||||||
|
pdf.sign(StringIO.new,
|
||||||
|
reason: 'Test',
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,6 +25,7 @@ class EncryptedConfig < ApplicationRecord
|
|||||||
FILES_STORAGE_KEY = 'active_storage',
|
FILES_STORAGE_KEY = 'active_storage',
|
||||||
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
EMAIL_SMTP_KEY = 'action_mailer_smtp',
|
||||||
ESIGN_CERTS_KEY = 'esign_certs',
|
ESIGN_CERTS_KEY = 'esign_certs',
|
||||||
|
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
|
||||||
APP_URL_KEY = 'app_url',
|
APP_URL_KEY = 'app_url',
|
||||||
WEBHOOK_URL_KEY = 'webhook_url'
|
WEBHOOK_URL_KEY = 'webhook_url'
|
||||||
].freeze
|
].freeze
|
||||||
|
|||||||
@@ -97,5 +97,31 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<% encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY) %>
|
||||||
|
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) %>
|
||||||
|
<div class="flex-grow max-w-xl">
|
||||||
|
<div class="flex justify-between items-end mb-4 mt-8">
|
||||||
|
<h2 class="text-3xl font-bold">Timestamp Server</h2>
|
||||||
|
</div>
|
||||||
|
<%= form_for encrypted_config, url: timestamp_server_index_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :value, class: 'label' do %>
|
||||||
|
<span class="flex items-center space-x-1">
|
||||||
|
<span>
|
||||||
|
Timeserver URL
|
||||||
|
</span>
|
||||||
|
<span class="tooltip" data-tip="URL of the trusted timeserver to be used to generate timestamp signatures.">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<%= f.url_field :value, autocomplete: 'off', class: 'base-input', placeholder: 'URL (optional)' %>
|
||||||
|
</div>
|
||||||
|
<div class="form-control pt-2">
|
||||||
|
<%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Rails.application.routes.draw do
|
|||||||
resources :verify_pdf_signature, only: %i[create]
|
resources :verify_pdf_signature, only: %i[create]
|
||||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||||
resources :account_configs, only: %i[create]
|
resources :account_configs, only: %i[create]
|
||||||
|
resources :timestamp_server, only: %i[create]
|
||||||
resources :dashboard, only: %i[index]
|
resources :dashboard, only: %i[index]
|
||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
resource :newsletter, only: %i[show update]
|
resource :newsletter, only: %i[show update]
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ module Accounts
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_timeserver_url(account)
|
||||||
|
if Docuseal.multitenant?
|
||||||
|
Docuseal::TIMESERVER_URL
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(account:, key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)&.value
|
||||||
|
end.presence
|
||||||
|
end
|
||||||
|
|
||||||
def can_send_emails?(_account)
|
def can_send_emails?(_account)
|
||||||
return true if Docuseal.multitenant?
|
return true if Docuseal.multitenant?
|
||||||
return true if ENV['SMTP_ADDRESS'].present?
|
return true if ENV['SMTP_ADDRESS'].present?
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ module Docuseal
|
|||||||
end
|
end
|
||||||
|
|
||||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||||
|
TIMESERVER_URL = ENV.fetch('TIMESERVER_URL', nil)
|
||||||
|
|
||||||
DEFAULT_URL_OPTIONS = {
|
DEFAULT_URL_OPTIONS = {
|
||||||
host: HOST,
|
host: HOST,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ module Submissions
|
|||||||
def call(submission)
|
def call(submission)
|
||||||
account = submission.template.account
|
account = submission.template.account
|
||||||
pkcs = Accounts.load_signing_pkcs(account)
|
pkcs = Accounts.load_signing_pkcs(account)
|
||||||
|
tsa_url = Accounts.load_timeserver_url(account)
|
||||||
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
|
verify_url = Rails.application.routes.url_helpers.settings_esign_url(**Docuseal.default_url_options)
|
||||||
|
|
||||||
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
composer = HexaPDF::Composer.new(skip_page_creation: true)
|
||||||
@@ -254,10 +255,16 @@ module Submissions
|
|||||||
|
|
||||||
composer.document.trailer.info[:Creator] = INFO_CREATOR
|
composer.document.trailer.info[:Creator] = INFO_CREATOR
|
||||||
|
|
||||||
composer.document.sign(io, reason: SIGN_REASON,
|
sign_params = {
|
||||||
certificate: pkcs.certificate,
|
reason: SIGN_REASON,
|
||||||
key: pkcs.key,
|
certificate: pkcs.certificate,
|
||||||
certificate_chain: pkcs.ca_certs || [])
|
key: pkcs.key,
|
||||||
|
certificate_chain: pkcs.ca_certs || []
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
|
||||||
|
|
||||||
|
composer.document.sign(io, **sign_params)
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(
|
blob: ActiveStorage::Blob.create_and_upload!(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ module Submissions
|
|||||||
|
|
||||||
account = submitter.submission.template.account
|
account = submitter.submission.template.account
|
||||||
pkcs = Accounts.load_signing_pkcs(account)
|
pkcs = Accounts.load_signing_pkcs(account)
|
||||||
|
tsa_url = Accounts.load_timeserver_url(account)
|
||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter)
|
pdfs_index = build_pdfs_index(submitter)
|
||||||
|
|
||||||
@@ -190,7 +191,9 @@ module Submissions
|
|||||||
submitter.submission.template_schema.map do |item|
|
submitter.submission.template_schema.map do |item|
|
||||||
pdf = pdfs_index[item['attachment_uuid']]
|
pdf = pdfs_index[item['attachment_uuid']]
|
||||||
|
|
||||||
attachment = save_signed_pdf(pdf:, submitter:, pkcs:, uuid: item['attachment_uuid'], name: item['name'])
|
attachment = save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:,
|
||||||
|
uuid: item['attachment_uuid'],
|
||||||
|
name: item['name'])
|
||||||
|
|
||||||
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
||||||
|
|
||||||
@@ -217,15 +220,21 @@ module Submissions
|
|||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:)
|
def save_signed_pdf(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
|
||||||
io = StringIO.new
|
io = StringIO.new
|
||||||
|
|
||||||
pdf.trailer.info[:Creator] = info_creator
|
pdf.trailer.info[:Creator] = info_creator
|
||||||
|
|
||||||
pdf.sign(io, reason: sign_reason(submitter.email),
|
sign_params = {
|
||||||
certificate: pkcs.certificate,
|
reason: sign_reason(submitter.email),
|
||||||
key: pkcs.key,
|
certificate: pkcs.certificate,
|
||||||
certificate_chain: pkcs.ca_certs || [])
|
key: pkcs.key,
|
||||||
|
certificate_chain: pkcs.ca_certs || []
|
||||||
|
}
|
||||||
|
|
||||||
|
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:) if tsa_url
|
||||||
|
|
||||||
|
pdf.sign(io, **sign_params)
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
uuid:,
|
uuid:,
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submissions
|
||||||
|
class TimestampHandler
|
||||||
|
HASH_ALGORITHM = 'SHA512'
|
||||||
|
|
||||||
|
TimestampError = Class.new(StandardError)
|
||||||
|
|
||||||
|
attr_reader :tsa_url
|
||||||
|
|
||||||
|
def initialize(tsa_url:)
|
||||||
|
@tsa_url = tsa_url
|
||||||
|
end
|
||||||
|
|
||||||
|
def finalize_objects(_signature_field, signature)
|
||||||
|
signature.document.version = '2.0'
|
||||||
|
|
||||||
|
signature[:Type] = :DocTimeStamp
|
||||||
|
signature[:Filter] = :'Adobe.PPKLite'
|
||||||
|
signature[:SubFilter] = :'ETSI.RFC3161'
|
||||||
|
end
|
||||||
|
|
||||||
|
def sign(io, byte_range)
|
||||||
|
digest = OpenSSL::Digest.new(HASH_ALGORITHM)
|
||||||
|
|
||||||
|
io.pos = byte_range[0]
|
||||||
|
digest << io.read(byte_range[1])
|
||||||
|
io.pos = byte_range[2]
|
||||||
|
digest << io.read(byte_range[3])
|
||||||
|
|
||||||
|
uri = Addressable::URI.parse(tsa_url)
|
||||||
|
|
||||||
|
conn = Faraday.new(uri.origin) do |c|
|
||||||
|
c.basic_auth(uri.user, uri.password) if uri.password.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
response = conn.post(uri.path, build_payload(digest.digest),
|
||||||
|
'content-type' => 'application/timestamp-query')
|
||||||
|
|
||||||
|
raise TimestampError if response.status != 200 || response.body.blank?
|
||||||
|
|
||||||
|
OpenSSL::Timestamp::Response.new(response.body).token.to_der
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_payload(digest)
|
||||||
|
req = OpenSSL::Timestamp::Request.new
|
||||||
|
req.algorithm = HASH_ALGORITHM
|
||||||
|
req.message_imprint = digest
|
||||||
|
|
||||||
|
req.to_der
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user