feat: Implement P12 certificate generation improvements

- Replace hardcoded P12 password with randomly generated one
- Add p12_password column to certificates table
- Update certificate serializer to include p12 password in response
- Remove deprecated certificate revocation logic
- Add tests for certificate revocation functionality
- Implement async P12 generation via Sidekiq job
- Add job uniqueness to prevent parallel certificate generation

Migration changes:
- Replace p12_password_digest with p12_password column
- Add safety measures for column removal
This commit is contained in:
oleghasjanov 2025-03-19 13:25:43 +02:00
parent 072f4440e2
commit ae96863b88
13 changed files with 157 additions and 64 deletions

View file

@ -4,6 +4,8 @@ class Certificate < ApplicationRecord
include Versions
include Certificate::CertificateConcern
self.ignored_columns = ["p12_password_digest"]
belongs_to :api_user
SIGNED = 'signed'.freeze
@ -20,6 +22,13 @@ class Certificate < ApplicationRecord
scope 'registrar', -> { where(interface: REGISTRAR) }
scope 'unrevoked', -> { where(revoked: false) }
# Базовые причины отзыва (самые частые)
REVOCATION_REASONS = {
unspecified: 0,
key_compromise: 1,
cessation_of_operation: 5
}.freeze
validate :validate_csr_and_crt_presence
def validate_csr_and_crt_presence
return if csr.try(:scrub).present? || crt.try(:scrub).present?
@ -65,14 +74,14 @@ class Certificate < ApplicationRecord
return nil if p12.blank?
decoded_p12 = Base64.decode64(p12)
OpenSSL::PKCS12.new(decoded_p12, Certificates::CertificateGenerator::P12_PASSWORD)
OpenSSL::PKCS12.new(decoded_p12, p12_password)
rescue OpenSSL::PKCS12::PKCS12Error => e
Rails.logger.error("Failed to parse PKCS12: #{e.message}")
nil
end
def revoked?
status == REVOKED
revoked
end
def revokable?
@ -81,17 +90,10 @@ class Certificate < ApplicationRecord
def status
return UNSIGNED if crt.blank?
return @cached_status if @cached_status
@cached_status = SIGNED
if certificate_expired?
@cached_status = EXPIRED
elsif certificate_revoked?
@cached_status = REVOKED
end
@cached_status
return REVOKED if revoked?
return EXPIRED if expires_at && expires_at < Time.current
SIGNED
end
def sign!(password:)
@ -106,18 +108,14 @@ class Certificate < ApplicationRecord
false
end
def revoke!(password:)
crt_file = create_tempfile('client_crt', crt)
def revoke!(password:, reason: :unspecified)
return false unless password == ENV['ca_key_password']
err_output = execute_openssl_revoke_command(password, crt_file.path)
if revocation_successful?(err_output)
update_revocation_status
self.class.update_crl
return self
end
handle_revocation_failure(err_output)
update!(
revoked: true,
revoked_at: Time.current,
revoked_reason: REVOCATION_REASONS[reason]
)
end
private

View file

@ -3,17 +3,6 @@ module Certificate::CertificateConcern
extend ActiveSupport::Concern
class_methods do
def tostdout(message)
time = Time.zone.now.utc
$stdout << "#{time} - #{message}\n" unless Rails.env.test?
end
def update_crl
tostdout('Running crlupdater')
system('/bin/bash', ENV['crl_updater_path'].to_s)
tostdout('Finished running crlupdater')
end
def parse_md_from_string(crt)
return if crt.blank?