diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb index 1cb2825cf..1faca4e68 100644 --- a/app/controllers/repp/v1/base_controller.rb +++ b/app/controllers/repp/v1/base_controller.rb @@ -150,11 +150,6 @@ module Repp crt = request.headers['User-Certificate'] com = request.headers['User-Certificate-CN'] - Rails.logger.info '============== crts ==============' - Rails.logger.info crt - Rails.logger.info com - Rails.logger.info '============== crts ==============' - return if @current_user.pki_ok?(crt, com, api: false) render_invalid_cert_response diff --git a/app/controllers/repp/v1/certificates_controller.rb b/app/controllers/repp/v1/certificates_controller.rb index 1403feb06..dccee8850 100644 --- a/app/controllers/repp/v1/certificates_controller.rb +++ b/app/controllers/repp/v1/certificates_controller.rb @@ -39,8 +39,8 @@ module Repp extension = case params[:type] when 'p12' then 'p12' when 'private_key' then 'key' - when 'csr' then 'csr' - when 'crt' then 'crt' + when 'csr' then 'csr.pem' + when 'crt' then 'crt.pem' else 'pem' end diff --git a/app/models/api_user.rb b/app/models/api_user.rb index 128cb6ee3..d6572999e 100644 --- a/app/models/api_user.rb +++ b/app/models/api_user.rb @@ -80,12 +80,6 @@ class ApiUser < User cert = machine_readable_certificate(crt) md5 = OpenSSL::Digest::MD5.new(cert.to_der).to_s - Rails.logger.info '============== pki_ok? ==============' - Rails.logger.info md5 - Rails.logger.info com - Rails.logger.info origin.exists?(md5: md5, common_name: com, revoked: false) - Rails.logger.info '============== pki_ok? ==============' - origin.exists?(md5: md5, common_name: com, revoked: false) end diff --git a/app/services/certificates/certificate_generator.rb b/app/services/certificates/certificate_generator.rb index bfea04977..a1f9a6613 100644 --- a/app/services/certificates/certificate_generator.rb +++ b/app/services/certificates/certificate_generator.rb @@ -102,10 +102,10 @@ module Certificates ca_key = OpenSSL::PKey::RSA.new(File.read(ca_key_path), ca_password) cert = OpenSSL::X509::Certificate.new - cert.serial = self.class.generate_serial_number # Используем новый метод генерации серийного номера + 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 год + cert.not_after = Time.now + 365 * 24 * 60 * 60 # TODO: 1 year (temporary) cert.subject = csr.subject cert.public_key = csr.public_key diff --git a/test/integration/repp/v1/certificates/create_test.rb b/test/integration/repp/v1/certificates/create_test.rb index 47b866f9f..7ead82706 100644 --- a/test/integration/repp/v1/certificates/create_test.rb +++ b/test/integration/repp/v1/certificates/create_test.rb @@ -40,7 +40,7 @@ class ReppV1CertificatesCreateTest < ActionDispatch::IntegrationTest json = JSON.parse(response.body, symbolize_names: true) assert_response :bad_request - assert json[:message].include? 'Invalid CSR or CRT' + assert json[:message].include? I18n.t(:crt_or_csr_must_be_present) end def test_returns_error_response_if_throttled diff --git a/test/models/certificate_test.rb b/test/models/certificate_test.rb index 6565b7a68..2319b4058 100644 --- a/test/models/certificate_test.rb +++ b/test/models/certificate_test.rb @@ -3,159 +3,10 @@ require 'test_helper' class CertificateTest < ActiveSupport::TestCase setup do @certificate = certificates(:api) - @valid_crt = <<~CRT - -----BEGIN CERTIFICATE----- - MIIDazCCAlOgAwIBAgIUBgtGh4Pw8Luqq/HG4tqG3oIzfHIwDQYJKoZIhvcNAQEL - BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM - GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDAyMTkxMjAwMDBaFw0yNTAy - MTkxMjAwMDBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw - HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB - AQUAA4IBDwAwggEKAoIBAQDUVURLKdmhmEht7yz3MeQQtn9kMIaIzZDwggZvUg6J - 5PlTabEixVfPzlRJixJBj37hh0Ree6mr19KECtPymy1L9U3oGfF18CJhdzc= - -----END CERTIFICATE----- - CRT - - @certificate.update!( - csr: "-----BEGIN CERTIFICATE REQUEST-----\nMIICszCCAZsCAQAwbjELMAkGA1UEBhMCRUUxFDASBgNVBAMMC2ZyZXNoYm94LmVl\nMRAwDgYDVQQHDAdUYWxsaW5uMREwDwYDVQQKDAhGcmVzaGJveDERMA8GA1UECAwI\nSGFyanVtYWExETAPBgNVBAsMCEZyZXNoYm94MIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA1VVESynZoZhIbe8s9zHkELZ/ZDCGiM2Q8IIGb1IOieT5U2mx\nIsVXz85USYsSQY9+4YdEXnupq9fShArT8pstS/VN6BnxdfAiYXc3UWWAuaYAdNGJ\nDr5Jf6uMt1wVnCgoDL7eJq9tWMwARC/viT81o92fgqHFHW0wEolfCmnpik9o0ACD\nFiWZ9IBIevmFqXtq25v9CY2cT9+eZW127WtJmOY/PKJhzh0QaEYHqXTHWOLZWpnp\nHH4elyJ2CrFulOZbHPkPNB9Nf4XQjzk1ffoH6e5IVys2VV5xwcTkF0jY5XTROVxX\nlR2FWqic8Q2pIhSks48+J6o1GtXGnTxv94lSDwIDAQABoAAwDQYJKoZIhvcNAQEL\nBQADggEBAEFcYmQvcAC8773eRTWBJJNoA4kRgoXDMYiiEHih5iJPVSxfidRwYDTF\nsP+ttNTUg3JocFHY75kuM9T2USh+gu/trRF0o4WWa+AbK3JbbdjdT1xOMn7XtfUU\nZ/f1XCS9YdHQFCA6nk4Z+TLWwYsgk7n490AQOiB213fa1UIe83qIfw/3GRqRUZ7U\nwIWEGsHED5WT69GyxjyKHcqGoV7uFnqFN0sQVKVTy/NFRVQvtBUspCbsOirdDRie\nAB2KbGHL+t1QrRF10szwCJDyk5aYlVhxvdI8zn010nrxHkiyQpDFFldDMLJl10BW\n2w9PGO061z+tntdRcKQGuEpnIr9U5Vs=\n-----END CERTIFICATE REQUEST-----\n", - private_key: "encrypted_private_key" - ) - end - - def test_does_metadata_is_api - api = @certificate.assign_metadata - assert api, 'api' + @certificate.update!(csr: "-----BEGIN CERTIFICATE REQUEST-----\nMIICszCCAZsCAQAwbjELMAkGA1UEBhMCRUUxFDASBgNVBAMMC2ZyZXNoYm94LmVl\nMRAwDgYDVQQHDAdUYWxsaW5uMREwDwYDVQQKDAhGcmVzaGJveDERMA8GA1UECAwI\nSGFyanVtYWExETAPBgNVBAsMCEZyZXNoYm94MIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA1VVESynZoZhIbe8s9zHkELZ/ZDCGiM2Q8IIGb1IOieT5U2mx\nIsVXz85USYsSQY9+4YdEXnupq9fShArT8pstS/VN6BnxdfAiYXc3UWWAuaYAdNGJ\nDr5Jf6uMt1wVnCgoDL7eJq9tWMwARC/viT81o92fgqHFHW0wEolfCmnpik9o0ACD\nFiWZ9IBIevmFqXtq25v9CY2cT9+eZW127WtJmOY/PKJhzh0QaEYHqXTHWOLZWpnp\nHH4elyJ2CrFulOZbHPkPNB9Nf4XQjzk1ffoH6e5IVys2VV5xwcTkF0jY5XTROVxX\nlR2FWqic8Q2pIhSks48+J6o1GtXGnTxv94lSDwIDAQABoAAwDQYJKoZIhvcNAQEL\nBQADggEBAEFcYmQvcAC8773eRTWBJJNoA4kRgoXDMYiiEHih5iJPVSxfidRwYDTF\nsP+ttNTUg3JocFHY75kuM9T2USh+gu/trRF0o4WWa+AbK3JbbdjdT1xOMn7XtfUU\nZ/f1XCS9YdHQFCA6nk4Z+TLWwYsgk7n490AQOiB213fa1UIe83qIfw/3GRqRUZ7U\nwIWEGsHED5WT69GyxjyKHcqGoV7uFnqFN0sQVKVTy/NFRVQvtBUspCbsOirdDRie\nAB2KbGHL+t1QrRF10szwCJDyk5aYlVhxvdI8zn010nrxHkiyQpDFFldDMLJl10BW\n2w9PGO061z+tntdRcKQGuEpnIr9U5Vs=\n-----END CERTIFICATE REQUEST-----\n") end def test_certificate_sign_returns_false - ENV['ca_key_password'] = 'test_password' - assert_not @certificate.sign!(password: ENV['ca_key_password']) + assert_not @certificate.sign!(password: ENV['ca_key_password']), 'false' end - - def test_renewable_when_not_expired - @certificate.update!( - crt: @valid_crt, - expires_at: 20.days.from_now - ) - - assert @certificate.renewable? - end - - def test_not_renewable_when_expired - @certificate.update!( - crt: @valid_crt, - expires_at: 1.day.ago - ) - - assert @certificate.expired? - assert_not @certificate.renewable? - end - - def test_generate_for_api_user - api_user = users(:api_bestnames) - - certificate = nil - assert_nothing_raised do - certificate = Certificate.generate_for_api_user(api_user: api_user) - end - - assert certificate.persisted? - assert_equal api_user, certificate.api_user - assert certificate.private_key.present? - assert certificate.csr.present? - assert certificate.expires_at.present? - end - - def test_certificate_revoked_when_crl_missing - crl_dir = ENV['crl_dir'] || Rails.root.join('ca/crl').to_s - crl_path = "#{crl_dir}/crl.pem" - - original_crl = nil - if File.exist?(crl_path) - original_crl = File.read(crl_path) - File.delete(crl_path) - end - - begin - File.delete(crl_path) if File.exist?(crl_path) - revoked = @certificate.respond_to?(:certificate_revoked?) ? @certificate.certificate_revoked? : nil - - if revoked != nil - assert_not revoked, "Certificate should not be considered revoked when CRL is missing" - end - ensure - if original_crl - FileUtils.mkdir_p(File.dirname(crl_path)) - File.write(crl_path, original_crl) - end - end - end - - def test_certificate_status - # Skip this test if the Certificate model doesn't have the status attribute in the database - skip unless Certificate.column_names.include?('status') - - # Now we know the column exists, so we can safely update it - @certificate.update(status: "signed") - assert_equal "signed", @certificate.status - assert_not @certificate.revoked?, "Certificate with 'signed' status should not be considered revoked" - - @certificate.update(status: "revoked") - assert_equal "revoked", @certificate.status - assert @certificate.revoked?, "Certificate with 'revoked' status should be considered revoked" - end - - def test_p12_status_with_properly_initialized_crl - # Skip if either certificate_revoked? method is missing or status column doesn't exist - skip unless (@certificate.respond_to?(:certificate_revoked?) && - Certificate.column_names.include?('status')) - - crl_dir = ENV['crl_dir'] || Rails.root.join('ca/crl').to_s - crl_path = "#{crl_dir}/crl.pem" - - original_crl = nil - if File.exist?(crl_path) - original_crl = File.read(crl_path) - end - - begin - FileUtils.mkdir_p(crl_dir) unless Dir.exist?(crl_dir) - File.write(crl_path, "-----BEGIN X509 CRL-----\nMIHsMIGTAgEBMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCVRlc3QgQ0EgMhcN\nMjQwNTEzMTcyMDM1WhcNMjUwNTEzMTcyMDM1WjBEMBMCAgPoFw0yMTA1MTMxNzIw\nMzVaMBMCAgPpFw0yMTA1MTMxNzIwMzVaMBMCAgPqFw0yMTA1MTMxNzIwMzVaMA0G\nCSqGSIb3DQEBCwUAA4GBAGX5rLzwJVAPhJ1iQZLFfzjwVJVGqDIZXt1odApM7/KA\nXrQ5YLVunSBGQTbuRQKNQZQO+snGnZUxJ5OW9eRqp8HWFpCFZbWSJ86eNfuX+GD3\nwgGP/1Zv+iRiZG8ccHQC4fNxQNctMFMccRVmcpOJ8s7h+Y5ohiUXyGTiLbBu4Np3\n-----END X509 CRL-----") - - assert_not @certificate.certificate_revoked?, "Certificate should not be considered revoked with an empty CRL" - - # Only update the status if the status column exists - @certificate.update(status: "signed") - assert_equal "signed", @certificate.status - - # Use a regular stub instead of Mocha if possible - if @certificate.respond_to?(:stubs) - @certificate.stubs(:certificate_revoked?).returns(true) - else - # Simple stub alternative - def @certificate.certificate_revoked?; true; end - end - - assert @certificate.certificate_revoked? - - # Skip the expectations part if we can't use Mocha - if defined?(Mocha) && @certificate.respond_to?(:expects) - if @certificate.respond_to?(:p12=) - @certificate.expects(:status=).with("revoked").at_least_once - end - end - ensure - if original_crl - File.write(crl_path, original_crl) - else - File.delete(crl_path) if File.exist?(crl_path) - end - - # Restore the original method if we used the simple stub - if !@certificate.respond_to?(:stubs) && @certificate.respond_to?(:certificate_revoked?) - class << @certificate - remove_method :certificate_revoked? - end - end - end - end -end \ No newline at end of file +end diff --git a/test/services/certificates/certificate_generator_test.rb b/test/services/certificates/certificate_generator_test.rb index 0154d9348..c00db6fb5 100644 --- a/test/services/certificates/certificate_generator_test.rb +++ b/test/services/certificates/certificate_generator_test.rb @@ -2,153 +2,88 @@ require 'test_helper' module Certificates class CertificateGeneratorTest < ActiveSupport::TestCase - setup do - @certificate = certificates(:api) - @generator = CertificateGenerator.new( - username: "test_user", - registrar_code: "REG123", - registrar_name: "Test Registrar" - ) + test "generate client private key" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + private_key = generator.generate_user_key + + assert_instance_of OpenSSL::PKey::RSA, private_key + assert_equal 4096, private_key.n.num_bits end - def test_generates_new_certificate - result = @generator.call - - assert result[:private_key].present? - assert result[:csr].present? - assert result[:crt].present? - assert result[:p12].present? - assert result[:expires_at].present? - - assert_instance_of String, result[:private_key] - assert_instance_of String, result[:csr] - assert_instance_of String, result[:crt] - assert_instance_of String, result[:p12] - assert_instance_of Time, result[:expires_at] + test "generate client csr" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + private_key = generator.generate_user_key + csr = generator.generate_user_csr(private_key) + + assert_instance_of OpenSSL::X509::Request, csr + assert csr.verify(csr.public_key) + assert_equal "/CN=#{generator.username}/OU=REGISTRAR/O=#{generator.registrar_name}", csr.subject.to_s end - def test_uses_existing_csr_and_private_key - existing_csr = @certificate.csr - existing_private_key = "existing_private_key" - @certificate.update!(private_key: existing_private_key) - - result = @generator.call - - assert result[:csr].present? - assert result[:private_key].present? - assert_not_equal existing_csr, result[:csr] - assert_not_equal existing_private_key, result[:private_key] + test "generate client ctr" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + private_key = generator.generate_user_key + csr = generator.generate_user_csr(private_key) + certificate = generator.sign_user_certificate(csr) + + ca_cert = OpenSSL::X509::Certificate.new(File.read(generator.ca_cert_path)) + + assert_instance_of OpenSSL::X509::Certificate, certificate + assert_equal csr.subject.to_s, certificate.subject.to_s + assert_equal 2, certificate.version + assert certificate.verify(ca_cert.public_key) end - def test_renew_certificate - @certificate.update!( - expires_at: 20.days.from_now - ) - - result = CertificateGenerator.new( - username: @certificate.common_name, - registrar_code: "REG123", - registrar_name: "Test Registrar" - ).call - - assert result[:crt].present? - assert result[:private_key].present? - end - - def test_generates_unique_serial_numbers - result1 = @generator.call - result2 = @generator.call - - cert1 = OpenSSL::X509::Certificate.new(result1[:crt]) - cert2 = OpenSSL::X509::Certificate.new(result2[:crt]) - - assert_not_equal 0, cert1.serial.to_i - assert_not_equal 0, cert2.serial.to_i - assert_not_equal cert1.serial.to_i, cert2.serial.to_i - end - - def test_serial_based_on_time - current_time = Time.now.to_i - - result = @generator.call - cert = OpenSSL::X509::Certificate.new(result[:crt]) + test "generate client p12" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + private_key = generator.generate_user_key + csr = generator.generate_user_csr(private_key) + certificate = generator.sign_user_certificate(csr) - # Check that the serial is at least around the current time - assert cert.serial.to_i >= current_time - 10 + p12 = generator.create_user_p12(private_key, certificate) - # Increase the upper bound to account for potential test execution delays - # and the random component added to the serial number - assert cert.serial.to_i <= current_time + 2000 - end - - def test_p12_creation_succeeds_with_crl - crl_dir = ENV['crl_dir'] || Rails.root.join('ca/crl').to_s - crl_path = "#{crl_dir}/crl.pem" - - original_crl = nil - if File.exist?(crl_path) - original_crl = File.read(crl_path) - end - - FileUtils.mkdir_p(crl_dir) unless Dir.exist?(crl_dir) + # Verify P12 can be loaded back with correct password + loaded_p12 = OpenSSL::PKCS12.new(p12, CertificateGenerator::P12_PASSWORD) - begin - if File.exist?(crl_path) - File.delete(crl_path) - end - - File.write(crl_path, "-----BEGIN X509 CRL-----\nMIHsMIGTAgEBMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCVRlc3QgQ0EgMhcN\nMjQwNTEzMTcyMDM1WhcNMjUwNTEzMTcyMDM1WjBEMBMCAgPoFw0yMTA1MTMxNzIw\nMzVaMBMCAgPpFw0yMTA1MTMxNzIwMzVaMBMCAgPqFw0yMTA1MTMxNzIwMzVaMA0G\nCSqGSIb3DQEBCwUAA4GBAGX5rLzwJVAPhJ1iQZLFfzjwVJVGqDIZXt1odApM7/KA\nXrQ5YLVunSBGQTbuRQKNQZQO+snGnZUxJ5OW9eRqp8HWFpCFZbWSJ86eNfuX+GD3\nwgGP/1Zv+iRiZG8ccHQC4fNxQNctMFMccRVmcpOJ8s7h+Y5ohiUXyGTiLbBu4Np3\n-----END X509 CRL-----") - - result = @generator.call - assert result[:p12].present? - - certificate = Certificate.last - assert_equal "signed", certificate.status if certificate.respond_to?(:status) - ensure - if original_crl - File.write(crl_path, original_crl) - end - end - end - - def test_p12_creation_with_missing_crl - crl_dir = ENV['crl_dir'] || Rails.root.join('ca/crl').to_s - crl_path = "#{crl_dir}/crl.pem" - - original_crl = nil - if File.exist?(crl_path) - original_crl = File.read(crl_path) - File.delete(crl_path) - end - - begin - File.delete(crl_path) if File.exist?(crl_path) - - result = @generator.call - assert result[:p12].present?, "P12 container should be created even when CRL is missing" - ensure - if original_crl - FileUtils.mkdir_p(File.dirname(crl_path)) - File.write(crl_path, original_crl) - end - end - end - - def test_certificate_status_in_db - result = @generator.call - - assert result[:crt].present? - assert result[:p12].present? - - if defined?(Certificate) && Certificate.method_defined?(:create_from_result) - certificate = Certificate.create_from_result(result) - assert_equal "signed", certificate.status if certificate.respond_to?(:status) - end - - assert_nothing_raised do - OpenSSL::X509::Certificate.new(result[:crt]) - end + assert_instance_of OpenSSL::PKCS12, loaded_p12 + assert_equal certificate.to_der, loaded_p12.certificate.to_der + assert_equal private_key.to_der, loaded_p12.key.to_der end + test "serial number should be created for each certificate" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + + # Generate two certificates and compare their serial numbers + csr1 = generator.generate_user_csr(generator.generate_user_key) + cert1 = generator.sign_user_certificate(csr1) + serial1 = cert1.serial.to_s(16) # Convert to hex string + + csr2 = generator.generate_user_csr(generator.generate_user_key) + cert2 = generator.sign_user_certificate(csr2) + serial2 = cert2.serial.to_s(16) # Convert to hex string + + assert_not_equal serial1, serial2 + assert_match(/^[0-9A-Fa-f]+$/, serial1) + assert_match(/^[0-9A-Fa-f]+$/, serial2) + end + + test "generated data should be store in database" do + generator = CertificateGenerator.new(api_user_id: users(:api_bestnames).id) + certificate_record = generator.execute + + assert certificate_record.persisted? + assert_not_nil certificate_record.private_key + assert_not_nil certificate_record.csr + assert_not_nil certificate_record.crt + assert_not_nil certificate_record.p12 + assert_equal 'registrar', certificate_record.interface + assert_equal CertificateGenerator::P12_PASSWORD, certificate_record.p12_password_digest + assert_equal users(:api_bestnames).username, certificate_record.common_name + + # Verify the certificate can be parsed back from stored data + cert = OpenSSL::X509::Certificate.new(certificate_record.crt) + assert_equal certificate_record.serial, cert.serial.to_s + assert_equal certificate_record.expires_at.to_i, cert.not_after.to_i + end end -end \ No newline at end of file +end