diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb index e19fd32db..1faca4e68 100644 --- a/app/controllers/repp/v1/base_controller.rb +++ b/app/controllers/repp/v1/base_controller.rb @@ -150,18 +150,8 @@ module Repp crt = request.headers['User-Certificate'] com = request.headers['User-Certificate-CN'] - Rails.logger.info "--------------------------------" - Rails.logger.info "Headers: crt=#{crt}, com=#{com}" - Rails.logger.info "test" - Rails.logger.info "#{@current_user.inspect}" - Rails.logger.info "--------------------------------" - return if @current_user.pki_ok?(crt, com, api: false) - Rails.logger.info "-------------------------------- FAIL" - Rails.logger.info @current_user.pki_ok?(crt, com, api: false) - Rails.logger.info "-------------------------------- FAIL" - render_invalid_cert_response end diff --git a/app/controllers/repp/v1/certificates/p12_controller.rb b/app/controllers/repp/v1/certificates/p12_controller.rb index 6a2c100fc..3b3e9f182 100644 --- a/app/controllers/repp/v1/certificates/p12_controller.rb +++ b/app/controllers/repp/v1/certificates/p12_controller.rb @@ -13,8 +13,7 @@ module Repp api_user_id = p12_params[:api_user_id] render_error(I18n.t('errors.messages.not_found'), :not_found) and return if api_user_id.blank? - api_user = current_user.registrar.api_users.find(api_user_id) - certificate = Certificate.generate_for_api_user(api_user: api_user) + certificate = ::Certificates::CertificateGenerator.new(api_user_id: api_user_id).execute render_success(data: { certificate: certificate }) end diff --git a/app/controllers/repp/v1/certificates_controller.rb b/app/controllers/repp/v1/certificates_controller.rb index 85c5c3746..f814941ef 100644 --- a/app/controllers/repp/v1/certificates_controller.rb +++ b/app/controllers/repp/v1/certificates_controller.rb @@ -20,65 +20,13 @@ module Repp def create @api_user = current_user.registrar.api_users.find(cert_params[:api_user_id]) - # Handle the invalid certificate test case explicitly - if the body is literally "invalid" - if cert_params[:csr] && cert_params[:csr][:body] == 'invalid' - @epp_errors = ActiveModel::Errors.new(self) - @epp_errors.add(:epp_errors, msg: 'Invalid CSR or CRT', code: '2304') - render_epp_error(:bad_request) and return - end - csr = decode_cert_params(cert_params[:csr]) - interface = cert_params[:interface].presence || 'api' - - # Проверяем, что CSR был успешно декодирован - if csr.nil? - @epp_errors = ActiveModel::Errors.new(self) - @epp_errors.add(:epp_errors, msg: I18n.t('errors.invalid_csr_format'), code: '2304') - render_epp_error(:bad_request) and return - end - - # Validate interface - unless Certificate::INTERFACES.include?(interface) - render_epp_error(:unprocessable_entity, message: I18n.t('errors.invalid_interface')) and return - end - # Validate CSR content to ensure it's a valid binary string before saving - unless csr.is_a?(String) && csr.valid_encoding? - @epp_errors = ActiveModel::Errors.new(self) - @epp_errors.add(:epp_errors, msg: I18n.t('errors.invalid_certificate'), code: '2304') - render_epp_error(:bad_request) and return - end - - @certificate = @api_user.certificates.build(csr: csr, interface: interface) + @certificate = @api_user.certificates.build(csr: csr) if @certificate.save - generator = ::Certificates::CertificateGenerator.new( - username: @api_user.username, - registrar_code: @api_user.registrar.code, - registrar_name: @api_user.registrar.name, - user_csr: csr, - interface: interface - ) - - result = generator.call - @certificate.update( - crt: result[:crt], - expires_at: result[:expires_at], - p12: result[:p12], - private_key: result[:private_key] - ) - - # Make sure we definitely call notify_admins notify_admins - render_success(data: { - certificate: { - id: @certificate.id, - common_name: @certificate.common_name, - expires_at: @certificate.expires_at, - interface: @certificate.interface, - status: @certificate.status - } - }) + render_success(data: { api_user: { id: @api_user.id } }) else handle_non_epp_errors(@certificate) end @@ -90,18 +38,11 @@ module Repp def download extension = params[:type] == 'p12' ? 'p12' : 'pem' filename = "#{@api_user.username}_#{Time.zone.today.strftime('%y%m%d')}_portal.#{extension}" - - # Добавим логирование для отладки - Rails.logger.info("Download certificate type: #{params[:type]}") - Rails.logger.info("Certificate has p12: #{@certificate.p12.present?}") - + data = if params[:type] == 'p12' && @certificate.p12.present? - Rails.logger.info("Decoding p12 from Base64") decoded = Base64.decode64(@certificate.p12) - Rails.logger.info("Decoded p12 size: #{decoded.bytesize} bytes") decoded else - Rails.logger.info("Using raw data from certificate: #{params[:type]}") @certificate[params[:type].to_s] end diff --git a/app/models/api_user.rb b/app/models/api_user.rb index 4793327e8..d6572999e 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -74,24 +74,11 @@ class ApiUser < User end def pki_ok?(crt, com, api: true) - Rails.logger.info '====== incoming params ======' - Rails.logger.info "crt: #{crt}\n\n" - Rails.logger.info "com: #{com}\n\n" - Rails.logger.info "api: #{api}\n\n" - Rails.logger.info "====== incoming params ======\n\n" - return false if crt.blank? || com.blank? - Rails.logger.info '====== handler ======' - origin = api ? certificates.api : certificates.registrar - Rails.logger.info "origin: #{origin}\n\n" cert = machine_readable_certificate(crt) - Rails.logger.info "cert: #{cert}\n\n" md5 = OpenSSL::Digest::MD5.new(cert.to_der).to_s - Rails.logger.info "md5: #{md5}\n\n" - - Rails.logger.info "====== handler ====== \n\n\n" origin.exists?(md5: md5, common_name: com, revoked: false) end diff --git a/app/models/certificate.rb b/app/models/certificate.rb index 29e682681..22a865cc7 100644 --- a/app/models/certificate.rb +++ b/app/models/certificate.rb @@ -52,34 +52,6 @@ class Certificate < ApplicationRecord @p_csr ||= OpenSSL::X509::Request.new(csr) if csr end - def parsed_private_key - return nil if private_key.blank? - - decoded_key = Base64.decode64(private_key) - OpenSSL::PKey::RSA.new(decoded_key, Certificates::CertificateGenerator.ca_password) - rescue OpenSSL::PKey::RSAError - nil - end - - # def parsed_p12 - # return nil if p12.blank? - - # decoded_p12 = Base64.decode64(p12) - # OpenSSL::PKCS12.new(decoded_p12) - # rescue OpenSSL::PKCS12::PKCS12Error - # nil - # end - - def parsed_p12 - return nil if p12.blank? - - decoded_p12 = Base64.decode64(p12) - OpenSSL::PKCS12.new(decoded_p12, '123456') - rescue OpenSSL::PKCS12::PKCS12Error => e - Rails.logger.error("Ошибка разбора PKCS12: #{e.message}") - nil - end - def revoked? status == REVOKED end @@ -96,7 +68,7 @@ class Certificate < ApplicationRecord if certificate_expired? @cached_status = EXPIRED - elsif revoked || certificate_revoked? + elsif certificate_revoked? @cached_status = REVOKED end @@ -129,109 +101,6 @@ class Certificate < ApplicationRecord handle_revocation_failure(err_output) end - def renewable? - return false if revoked? - return false if crt.blank? - return false if expires_at.blank? - - expires_at > Time.current && expires_at <= 30.days.from_now - end - - def expired? - return false if revoked? - return false if crt.blank? - return false if expires_at.blank? - - expires_at < Time.current - end - - def renew - raise "Certificate cannot be renewed" unless renewable? - - generator = Certificates::CertificateGenerator.new( - username: api_user.username, - registrar_code: api_user.registrar_code, - registrar_name: api_user.registrar_name, - certificate: self - ) - - generator.renew_certificate - end - - def self.generate_for_api_user(api_user:) - generator = Certificates::CertificateGenerator.new( - username: api_user.username, - registrar_code: api_user.registrar_code, - registrar_name: api_user.registrar_name - ) - - cert_data = generator.call - - create!( - api_user: api_user, - interface: 'api', - private_key: Base64.encode64(cert_data[:private_key]), - csr: cert_data[:csr], - crt: cert_data[:crt], - p12: Base64.encode64(cert_data[:p12]), - expires_at: cert_data[:expires_at] - ) - end - - def self.diagnose_crl_issue - crl_path = "#{ENV['crl_dir']}/crl.pem" - - Rails.logger.info("CRL path: #{crl_path}") - Rails.logger.info("CRL exists: #{File.exist?(crl_path)}") - - return false unless File.exist?(crl_path) - - crl = OpenSSL::X509::CRL.new(File.open(crl_path).read) - - Rails.logger.info("CRL issuer: #{crl.issuer}") - Rails.logger.info("CRL last update: #{crl.last_update}") - Rails.logger.info("CRL next update: #{crl.next_update}") - Rails.logger.info("CRL revoked certificates count: #{crl.revoked.size}") - - if crl.revoked.any? - crl.revoked.each do |revoked| - Rails.logger.info("Revoked serial: #{revoked.serial}, time: #{revoked.time}") - end - end - - true - rescue => e - Rails.logger.error("Error parsing CRL file: #{e.message}") - false - end - - def self.regenerate_crl - ca_base_path = ENV['ca_dir'] || Rails.root.join('certs', 'ca').to_s - - command = [ - 'openssl', 'ca', - '-config', ENV['openssl_config_path'] || "#{ca_base_path}/openssl.cnf", - '-keyfile', ENV['ca_key_path'] || "#{ca_base_path}/private/ca.key.pem", - '-cert', ENV['ca_cert_path'] || "#{ca_base_path}/certs/ca.crt.pem", - '-crldays', '3650', - '-gencrl', - '-out', ENV['crl_path'] || "#{ca_base_path}/crl/crl.pem" - ] - - output, error, status = Open3.capture3(*command) - - if status.success? - Rails.logger.info("CRL regenerated successfully") - true - else - Rails.logger.error("Failed to regenerate CRL: #{error}") - false - end - rescue => e - Rails.logger.error("Error in regenerate_crl: #{e.message}") - false - end - private def certificate_origin @@ -325,28 +194,7 @@ class Certificate < ApplicationRecord end def certificate_revoked? - return true if revoked - - crl_path = "#{ENV['crl_dir']}/crl.pem" - return false unless File.exist?(crl_path) - - crl = OpenSSL::X509::CRL.new(File.open(crl_path).read) - - if crl.next_update && crl.next_update < Time.now - Rails.logger.warn("CRL file is expired! next_update: #{crl.next_update}") - return false - end - - cert_serial = parsed_crt.serial - is_revoked = crl.revoked.map(&:serial).include?(cert_serial) - - if is_revoked - Rails.logger.warn("Certificate with serial #{cert_serial} found in CRL!") - end - - is_revoked - rescue => e - Rails.logger.error("Error checking CRL: #{e.message}") - false + crl = OpenSSL::X509::CRL.new(File.open("#{ENV['crl_dir']}/crl.pem").read) + crl.revoked.map(&:serial).include?(parsed_crt.serial) end -end \ No newline at end of file +end diff --git a/app/services/certificates/certificate_generator.rb b/app/services/certificates/certificate_generator.rb index 43bc24c1d..a7e6c954e 100644 --- a/app/services/certificates/certificate_generator.rb +++ b/app/services/certificates/certificate_generator.rb @@ -1,21 +1,44 @@ module Certificates class CertificateGenerator < Dry::Struct - attribute :username, Types::Strict::String - attribute :registrar_code, Types::Coercible::String - attribute :registrar_name, Types::Strict::String - attribute? :user_csr, Types::Any.optional + attribute :api_user_id, Types::Coercible::Integer attribute? :interface, Types::String.optional - CERTS_PATH = Rails.root.join('certs') - CA_PATH = CERTS_PATH.join('ca') + P12_PASSWORD = 'todo-change-me' + + def execute + api_user = ApiUser.find(api_user_id) + + private_key = generate_user_key + csr = generate_user_csr(private_key) + certificate = sign_user_certificate(csr) + p12 = create_user_p12(private_key, certificate) + + certificate_record = api_user.certificates.build( + private_key: private_key.to_pem, + csr: csr.to_pem, + crt: certificate.to_pem, + p12: Base64.strict_encode64(p12), + expires_at: certificate.not_after, + interface: interface || 'registrar', + p12_password_digest: P12_PASSWORD, + serial: certificate.serial.to_s, + common_name: api_user.username + ) + + certificate_record.save! + certificate_record + end + + def self.generate_serial_number + serial = Time.now.to_i.to_s + Random.rand(10000).to_s.rjust(5, '0') + + OpenSSL::BN.new(serial) + end + + def self.openssl_config_path + ENV['openssl_config_path'] || Rails.root.join('test/fixtures/files/test_ca/openssl.cnf').to_s + end - # User certificate files - USER_CSR_NAME = 'user.csr' - USER_KEY_NAME = 'user.key' - USER_CRT_NAME = 'user.crt' - USER_P12_NAME = 'user.p12' - - # CA файлы - используем методы вместо констант для возможности переопределения из ENV def self.ca_cert_path ENV['ca_cert_path'] || Rails.root.join('test/fixtures/files/test_ca/certs/ca.crt.pem').to_s end @@ -28,7 +51,6 @@ module Certificates ENV['ca_key_password'] || '123456' end - # Методы экземпляра для доступа к CA путям def ca_cert_path self.class.ca_cert_path end @@ -41,69 +63,25 @@ module Certificates self.class.ca_password end - def initialize(*) - super - ensure_directories_exist + def openssl_config_path + self.class.openssl_config_path + end + + def username + @username ||= ApiUser.find(api_user_id).username end - def call - if user_csr - # Use provided CSR - it's already decoded in the controller - begin - csr = create_request_from_raw_csr(user_csr) - key = generate_key - save_csr(csr) - rescue => e - Rails.logger.error("Error parsing CSR: #{e.message}") - # Fall back to generating our own CSR and key - csr, key = generate_csr_and_key - end - else - # Generate new CSR and key - csr, key = generate_csr_and_key - end - - cert = sign_certificate(csr) - - # Always create p12 when we have a key, regardless of user_csr - p12_data = create_p12(key, cert) - - result = { - csr: csr.to_pem, - crt: cert.to_pem, - expires_at: cert.not_after, - private_key: key.export(OpenSSL::Cipher.new('AES-256-CBC'), ca_password), - p12: p12_data - } - - result + def registrar_name + @registrar_name ||= ApiUser.find(api_user_id).registrar_name end - - private - - def create_request_from_raw_csr(raw_csr) - # The CSR is already decoded in the controller - # Just ensure it's in the proper format - csr_text = raw_csr.to_s - - # Make sure it has proper BEGIN/END markers - unless csr_text.include?("-----BEGIN CERTIFICATE REQUEST-----") - csr_text = "-----BEGIN CERTIFICATE REQUEST-----\n#{csr_text}\n-----END CERTIFICATE REQUEST-----" - end - - OpenSSL::X509::Request.new(csr_text) - rescue => e - Rails.logger.error("Failed to parse CSR: #{e.message}") - raise - end - - def generate_key + + # openssl genrsa -out ./ca/client/client.key 4096 + def generate_user_key OpenSSL::PKey::RSA.new(4096) end - def generate_csr_and_key - key = generate_key - + # openssl req -new -key ./ca/client/client.key -out ./ca/client/client.csr -config ./ca/openssl.cnf + def generate_user_csr(key) request = OpenSSL::X509::Request.new request.version = 0 request.subject = OpenSSL::X509::Name.new([ @@ -115,152 +93,49 @@ module Certificates request.public_key = key.public_key request.sign(key, OpenSSL::Digest::SHA256.new) - save_csr(request) - save_private_key(key.export(OpenSSL::Cipher.new('AES-256-CBC'), ca_password)) - - [request, key] + request end - def sign_certificate(csr) - begin - ca_key_content = File.read(ca_key_path) - Rails.logger.debug("CA key file exists and has size: #{ca_key_content.size} bytes") - - # Try different password combinations - passwords_to_try = [ca_password, '', 'changeit', 'password'] - - ca_key = nil - last_error = nil - - passwords_to_try.each do |password| - begin - ca_key = OpenSSL::PKey::RSA.new(ca_key_content, password) - Rails.logger.debug("Successfully loaded CA key with password: #{password == ca_password ? 'default' : password}") - break - rescue => e - last_error = e - Rails.logger.debug("Failed to load CA key with password: #{password == ca_password ? 'default' : password}, error: #{e.message}") - end - end - - # If we still couldn't load the key, try without encryption headers - if ca_key.nil? - begin - # Remove encryption headers and try without a password - simplified_key = ca_key_content.gsub(/Proc-Type:.*\n/, '') - .gsub(/DEK-Info:.*\n/, '') - ca_key = OpenSSL::PKey::RSA.new(simplified_key) - Rails.logger.debug("Successfully loaded CA key after removing encryption headers") - rescue => e - Rails.logger.debug("Failed to load CA key after removing encryption headers: #{e.message}") - raise last_error || e - end - end - - ca_cert = OpenSSL::X509::Certificate.new(File.read(ca_cert_path)) - - cert = OpenSSL::X509::Certificate.new - cert.serial = Time.now.to_i + Random.rand(1000) - cert.version = 2 - cert.not_before = Time.now - cert.not_after = Time.now + 365 * 24 * 60 * 60 # 1 year - - cert.subject = csr.subject - cert.public_key = csr.public_key - cert.issuer = ca_cert.subject - - extension_factory = OpenSSL::X509::ExtensionFactory.new - extension_factory.subject_certificate = cert - extension_factory.issuer_certificate = ca_cert - - cert.add_extension(extension_factory.create_extension("basicConstraints", "CA:FALSE")) - cert.add_extension(extension_factory.create_extension("keyUsage", "nonRepudiation,digitalSignature,keyEncipherment")) - cert.add_extension(extension_factory.create_extension("subjectKeyIdentifier", "hash")) - - cert.sign(ca_key, OpenSSL::Digest::SHA256.new) - save_certificate(cert) - - cert - rescue => e - Rails.logger.error("Error signing certificate: #{e.message}") - Rails.logger.error("CA key path: #{ca_key_path}, exists: #{File.exist?(ca_key_path)}") - - # For test purposes, we'll create a self-signed certificate as a fallback - key = generate_key - cert = OpenSSL::X509::Certificate.new - cert.version = 2 - cert.serial = 0 - name = OpenSSL::X509::Name.new([['CN', username]]) - cert.subject = name - cert.issuer = name - cert.not_before = Time.now - cert.not_after = Time.now + 365 * 24 * 60 * 60 - cert.public_key = key.public_key - ef = OpenSSL::X509::ExtensionFactory.new - ef.subject_certificate = cert - ef.issuer_certificate = cert - cert.extensions = [ - ef.create_extension("basicConstraints", "CA:FALSE", true), - ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true) - ] - cert.sign(key, OpenSSL::Digest::SHA256.new) - save_certificate(cert) - - cert - end - end - - def create_p12(key, cert) + # openssl ca -config ./ca/openssl.cnf -keyfile ./ca/ca.key.pem -cert ./ca/ca_2025.pem -extensions usr_cert -notext -md sha256 -in ./ca/client/client.csr -out ./ca/client/client.crt -batch + def sign_user_certificate(csr) ca_cert = OpenSSL::X509::Certificate.new(File.read(ca_cert_path)) + ca_key = OpenSSL::PKey::RSA.new(File.read(ca_key_path), ca_password) - Rails.logger.info("Creating PKCS12 container for #{username}") - Rails.logger.info("Certificate Subject: #{cert.subject}") - Rails.logger.info("Certificate Issuer: #{cert.issuer}") + cert = OpenSSL::X509::Certificate.new + cert.serial = self.class.generate_serial_number # Используем новый метод генерации серийного номера + cert.version = 2 + cert.not_before = Time.now + cert.not_after = Time.now + 365 * 24 * 60 * 60 # 1 год - begin - # Используем стандартные алгоритмы для максимальной совместимости - p12 = OpenSSL::PKCS12.create( - '123456', # Оставляем тот же пароль - username, - key, - cert, - [ca_cert] - # Убираем явное указание алгоритмов и итераций - ) - rescue => e - Rails.logger.error("Error creating PKCS12: #{e.message}") - raise - end + cert.subject = csr.subject + cert.public_key = csr.public_key + cert.issuer = ca_cert.subject - File.open(CERTS_PATH.join(USER_P12_NAME), 'wb') do |file| - file.write(p12.to_der) - end + extension_factory = OpenSSL::X509::ExtensionFactory.new + extension_factory.subject_certificate = cert + extension_factory.issuer_certificate = ca_cert - Rails.logger.info("Created PKCS12 with standard algorithms (size: #{p12.to_der.bytesize} bytes)") + cert.add_extension(extension_factory.create_extension("basicConstraints", "CA:FALSE")) + cert.add_extension(extension_factory.create_extension("keyUsage", "nonRepudiation,digitalSignature,keyEncipherment")) + cert.add_extension(extension_factory.create_extension("subjectKeyIdentifier", "hash")) + + cert.sign(ca_key, OpenSSL::Digest::SHA256.new) + + cert + end + + def create_user_p12(key, cert, password = '123456') + ca_cert = OpenSSL::X509::Certificate.new(File.read(ca_cert_path)) + + p12 = OpenSSL::PKCS12.create( + password, + username, + key, + cert, + [ca_cert] + ) - # Возвращаем бинарные данные p12.to_der end - - def ensure_directories_exist - FileUtils.mkdir_p(CERTS_PATH) - FileUtils.mkdir_p(CA_PATH.join('certs')) - FileUtils.mkdir_p(CA_PATH.join('private')) - FileUtils.chmod(0700, CA_PATH.join('private')) - end - - def save_private_key(encrypted_key) - path = CERTS_PATH.join(USER_KEY_NAME) - File.write(path, encrypted_key) - FileUtils.chmod(0600, path) - end - - def save_csr(request) - File.write(CERTS_PATH.join(USER_CSR_NAME), request.to_pem) - end - - def save_certificate(certificate) - File.write(CERTS_PATH.join(USER_CRT_NAME), certificate.to_pem) - end end end \ No newline at end of file diff --git a/db/migrate/20250313122119_add_serial_and_revoke_states_to_certificates.rb b/db/migrate/20250313122119_add_serial_and_revoke_states_to_certificates.rb new file mode 100644 index 000000000..6b6f0297f --- /dev/null +++ b/db/migrate/20250313122119_add_serial_and_revoke_states_to_certificates.rb @@ -0,0 +1,7 @@ +class AddSerialAndRevokeStatesToCertificates < ActiveRecord::Migration[6.1] + def change + add_column :certificates, :serial, :string, null: true + add_column :certificates, :revoked_at, :datetime, null: true + add_column :certificates, :revoked_reason, :integer, null: true + end +end diff --git a/db/structure.sql b/db/structure.sql index 68869987f..4d1270cd1 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -591,7 +591,10 @@ CREATE TABLE public.certificates ( private_key bytea, p12 bytea, p12_password_digest character varying, - expires_at timestamp without time zone + expires_at timestamp without time zone, + serial character varying, + revoked_at timestamp without time zone, + revoked_reason integer ); @@ -5723,6 +5726,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20241129095711'), ('20241206085817'), ('20250204094550'), -('20250219102811'); +('20250219102811'), +('20250313122119');