Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions app/controllers/repp/v1/certificates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def create
@api_user = current_user.registrar.api_users.find(cert_params[:api_user_id])

csr = decode_cert_params(cert_params[:csr])
validation_error = validate_csr_subject(csr, @api_user)
return handle_non_epp_errors(@certificate || Certificate.new, validation_error) if validation_error.present?

@certificate = @api_user.certificates.build(csr: csr)

Expand All @@ -37,22 +39,22 @@ def create
param :type, String, required: true, desc: 'Type of certificate (csr or crt)'
def download
extension = case params[:type]
when 'p12' then 'p12'
when 'private_key' then 'key'
when 'csr' then 'csr.pem'
when 'crt' then 'crt.pem'
else 'pem'
end
when 'p12' then 'p12'
when 'private_key' then 'key'
when 'csr' then 'csr.pem'
when 'crt' then 'crt.pem'
else 'pem'
end

filename = "#{@api_user.username}_#{Time.zone.today.strftime('%y%m%d')}_portal.#{extension}"

data = if params[:type] == 'p12' && @certificate.p12.present?
decoded = Base64.decode64(@certificate.p12)
decoded
else
@certificate[params[:type].to_s]
end
decoded = Base64.decode64(@certificate.p12)
decoded
else
@certificate[params[:type].to_s]
end

send_data data, filename: filename
end

Expand Down Expand Up @@ -83,14 +85,36 @@ def decode_cert_params(csr_params)

def sanitize_base64(text)
return '' if text.blank?

text = text.to_s.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
text.gsub(/\s+/, '')
end

def validate_csr_subject(csr, api_user)
return I18n.t(:crt_or_csr_must_be_present) if csr.blank?

request = OpenSSL::X509::Request.new(csr)
csr_cn = csr_subject_value(request, 'CN')
return I18n.t(:csr_cn_mismatch) unless csr_cn == api_user.username

csr_country = csr_subject_value(request, 'C')
return if csr_country.blank?

registrar_country = api_user.registrar.address_country_code.to_s.upcase
return if csr_country.upcase == registrar_country

I18n.t(:csr_country_mismatch)
rescue OpenSSL::X509::RequestError
I18n.t(:invalid_csr_or_crt)
end

def csr_subject_value(request, key)
request.subject.to_a.find { |entry| entry[0] == key }&.[](1)&.to_s&.strip
end

def notify_admins
admin_users_emails = AdminUser.pluck(:email).reject(&:blank?)

return if admin_users_emails.empty?

admin_users_emails.each do |email|
Expand Down
4 changes: 4 additions & 0 deletions app/interactions/actions/domain_create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ def call
assign_registrant
assign_nameservers
assign_domain_contacts
# Do not call attach_default_contacts: from 1 Feb 2025 .ee domain rules, admin and
# technical contacts are optional (tech is always optional; admin only under
# specific conditions). Previously, org registrants were auto-assigned as tech
# contact when none were provided; that behaviour is no longer valid.
# domain.attach_default_contacts
assign_expiry_time
maybe_attach_legal_doc
Expand Down
3 changes: 3 additions & 0 deletions app/models/epp/domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ def epp_code_map
}
end

# Legacy helper: auto-assigned registrant as tech (org) or admin (non-org) when missing.
# Superseded by .ee rules effective 1 Feb 2025 (optional tech/admin). Not used by
# DomainCreate/REPP; kept until callers are removed.
def attach_default_contacts
return if registrant.blank?

Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ en:
not_valid_domain_verification_body: This could mean your verification has been expired or done already.<br><br>Please contact us if you think something is wrong.
upload_crt: 'Upload CRT'
crt_or_csr_must_be_present: 'CRT or CSR must be present'
csr_cn_mismatch: 'CSR common name (CN) must match API user username'
csr_country_mismatch: 'CSR country (C) must match registrar country'
white_ip: 'White IP'
edit_white_ip: 'Edit white IP'
confirm_domain_delete: 'Confirm domain delete'
Expand Down
4 changes: 4 additions & 0 deletions config/locales/et.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ et:
taken: 'on juba lisatud'

invalid_ident: 'Jõustatud kustutamine määratud domainile %{domain_name}. Jõustatud kustutamine tüüp: %{force_delete_type}. Jõustatud kustutamise algus kuupäev: %{force_delete_start_date}. Väljaandmise kuupäev: %{outzone_date}. Kustutamise kuupäev: %{purge_date}. Vigane ident %{ident}'
crt_or_csr_must_be_present: 'CRT või CSR peab olema esitatud'
invalid_csr_or_crt: 'Vigane CSR või CRT'
csr_cn_mismatch: 'CSR common name (CN) peab kattuma API kasutaja kasutajanimega'
csr_country_mismatch: 'CSR country (C) peab kattuma registripidaja riigiga'
62 changes: 38 additions & 24 deletions test/integration/repp/v1/certificates/create_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'test_helper'
require 'openssl'

class ReppV1CertificatesCreateTest < ActionDispatch::IntegrationTest
def setup
Expand All @@ -25,6 +26,26 @@ def test_creates_new_api_user_certificate_and_informs_admins
assert_equal 'Command completed successfully', json[:message]
end

def test_returns_error_when_csr_cn_does_not_match_api_username
body = request_body(username: 'wrong_user')

post repp_v1_certificates_path, headers: @auth_headers, params: body
json = JSON.parse(response.body, symbolize_names: true)

assert_response :bad_request
assert_includes json[:message], I18n.t(:csr_cn_mismatch)
end

def test_returns_error_when_csr_country_does_not_match_registrar_country
body = request_body(country: 'LV')

post repp_v1_certificates_path, headers: @auth_headers, params: body
json = JSON.parse(response.body, symbolize_names: true)

assert_response :bad_request
assert_includes json[:message], I18n.t(:csr_country_mismatch)
end

def test_return_error_when_invalid_certificate
request_body = {
certificate: {
Expand Down Expand Up @@ -58,37 +79,30 @@ def test_returns_error_response_if_throttled
ENV['shunter_enabled'] = 'false'
end

def request_body
def request_body(username: @user.username, country: @user.registrar.address_country_code)
csr_body = Base64.strict_encode64(generate_csr_pem(username: username, country: country))

{
certificate: {
api_user_id: @user.id,
csr: {
body: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQ3dqQ0NB\n" \
"YW9DQVFBd2ZURUxNQWtHQTFVRUJoTUNSVlF4RVRBUEJnTlZCQWdNQ0VoaGNt\n" \
"cDFiV0ZoTVJBdwpEZ1lEVlFRSERBZFVZV3hzYVc1dU1SUXdFZ1lEVlFRS0RB\n" \
"dEpiblJsY201bGRDNWxaVEVRTUE0R0ExVUVBd3dICmFHOXpkQzVsWlRFaE1C\n" \
"OEdDU3FHU0liM0RRRUpBUllTYzJWeVoyVnBkRFpBWjIxaGFXd3VZMjl0TUlJ\n" \
"QklqQU4KQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBdk80\n" \
"UWltNlFxUzFRWVVRNjFUbGk0UG9DTTlhZgp4dUI5ZFM4endMb2hsOWhSOWdI\n" \
"dGJmcHpwSk5hLzlGeW0zcUdUZ3V0eVd3VGtWV3FzL0o3UjVpckxaY1pKaXI4\n" \
"CnZMZEo4SWlKL3ZTRDdNeS9oNzRRdHFGZlNNSi85bzAyUkJRdVFSWUU4Z3hU\n" \
"ZTRiMjU5NUJVQnZIUTFyczQxaGoKLzJ6SytuRDBsbHVvUFdrNnBCZ1NGZkN1\n" \
"Y0tWcE44Tm5vZUdGUjRnWHJQT0t2bkMwb3BxNi9SWmJxYm9hbTkxZwpWYWJ0\n" \
"Y0t4d3pmd2kxUlYzUUVxRXRUY0QvS0NwTzJRMTVXR3FtN2ZFYVMwVlZCckZw\n" \
"bzZWanZCSXUxRXJvcWJZCnBRaE9MZSt2RUh2bXFTS2JhZmFGTC9ZNHZyaU9P\n" \
"aU5yS01LTnR3cmVzeUI5TVh4YlNlMG9LSE1IVndJREFRQUIKb0FBd0RRWUpL\n" \
"b1pJaHZjTkFRRUxCUUFEZ2dFQkFKdEViWnlXdXNaeis4amVLeVJzL1FkdXNN\n" \
"bEVuV0RQTUdhawp3cllBbTVHbExQSEEybU9TUjkwQTY5TFBtY1FUVUtTTVRa\n" \
"NDBESjlnS2IwcVM3czU2UVFzblVQZ0hPMlFpWDlFCjZRcnVSTzNJN2kwSHZO\n" \
"K3g1Q29qUHBwQTNHaVdBb0dObG5uaWF5ZTB1UEhwVXFLbUcwdWFmVUpXS2tL\n" \
"Vi9vN3cKQXBIQWlQU0lLNHFZZ1FtZDBOTTFmM0FBL21pRi9xa3lZVGMya05s\n" \
"bG5DNm9vdldmV2hvSjdUdWluaE9Ka3BaaAp6YksxTHVoQ0FtWkNCVHowQmRt\n" \
"R2szUmVKL2dGTGpHWC9qd3BQRURPRGJHdkpYSzFuZzBwbXFlOFZzSms2SVYz\n" \
"Ckw0T3owY1JzTTc1UGtQbGloQ3RJOEJGQk04YVhCZjJ6QXZiV0NpY3piWTRh\n" \
"enBzc3VMbz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tCg==\n",
body: csr_body,
type: 'csr',
},
},
}
end

def generate_csr_pem(username:, country:)
key = OpenSSL::PKey::RSA.new(2048)
request = OpenSSL::X509::Request.new
request.version = 0
request.subject = OpenSSL::X509::Name.new([
['CN', username, OpenSSL::ASN1::UTF8STRING],
['C', country.to_s.upcase, OpenSSL::ASN1::PRINTABLESTRING],
])
request.public_key = key.public_key
request.sign(key, OpenSSL::Digest.new('SHA256'))
request.to_pem
end
end
Loading