diff --git a/Gemfile b/Gemfile index fb0fa71..c062096 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,9 @@ gem "vault" # Use the jwt gem to decode access tokens gem "jwt" +# Use the jbuilder gem +gem "jbuilder" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index 217c45e..e3d37ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,9 @@ GEM irb (1.14.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + jbuilder (2.12.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) json (2.7.2) jwt (2.8.2) base64 @@ -272,6 +275,7 @@ DEPENDENCIES brakeman debug interactor (~> 3.0) + jbuilder jwt puma (>= 5.0) rails (~> 7.2.1) diff --git a/README.md b/README.md index 03ab0fa..311201e 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ Astral is an api-only application intended to simplify certificate acquisition for other applications/services. Broadly speaking, it will: -1) Authorize the request for cerficate using a third party trusted source (JWT, etc) -2) If authorized, obtain a certificate from PKI CLM (such as Vault/OpenBao) -3) Log this transaction in audit infrastructure (ELK, etc). +1) Authenticate the request for cerficate using a third party trusted source (JWT, etc) +2) Authorize the request using a Domain Ownership Registry +3) If authorized, obtain a certificate from PKI CLM (such as Vault/OpenBao) +4) Log this transaction in audit infrastructure (ELK, etc). -# Running +# Running in development This Rails app is most easily run and developed in its devcontainer. @@ -22,9 +23,19 @@ rails s curl -X POST http://localhost:3000/certificates \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI" \ -H "Content-type: application/json" \ --d "{ \"common_name\": \"example.com\" }" +-d "{ \"cert_issue_request\": { \"common_name\": \"example.com\" } }" ``` 4) Run the tests from devcontainer terminal: ``` rails test ``` + +# Running the prod image +1) Build the prod image: +``` +docker build -t astral:latest . +``` +2) Run the prod image: +``` +docker run -e SECRET_KEY_BASE=mysecrit -p 3000:3000 astral:latest +``` diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2a48601..9c01126 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,18 +1,11 @@ class ApplicationController < ActionController::API + before_action :set_default_format rescue_from StandardError, with: :handle_standard_error rescue_from AuthError, with: :handle_auth_error rescue_from ActionController::ParameterMissing, with: :handle_bad_request attr_reader :identity # decoded and verified JWT - def info - render json: { - app: "astral", - description: "Astral provides a simplified API for PKI.", - version: "0.0.1" - } - end - def authenticate_request result = AuthenticateIdentity.call(request: request) if result.success? @@ -24,6 +17,10 @@ def authenticate_request private + def set_default_format + request.format = :json + end + def handle_standard_error(exception) render json: { error: exception.message }, status: :internal_server_error end diff --git a/app/controllers/certificates_controller.rb b/app/controllers/certificates_controller.rb index 2deae83..3b3b3f6 100644 --- a/app/controllers/certificates_controller.rb +++ b/app/controllers/certificates_controller.rb @@ -5,36 +5,19 @@ def create req = CertIssueRequest.new(params_permitted) if !req.valid? render json: { error: req.errors }, status: :bad_request + return end result = IssueCert.call(request: req) - if result.success? - # TODO use jbuilder to make the json - render json: result.cert - else + if result.failure? raise StandardError.new result.message end + @cert = result.cert end private def params_permitted - attrs = %i[ common_name - alt_names - exclude_cn_from_sans - format - not_after - other_sans - private_key_format - remove_roots_from_chain - ttl - uri_sans - ip_sans - serial_number - client_flag - code_signing_flag - email_protection_flag - server_flag - ] - params.permit(attrs) + attrs = CertIssueRequest.new.attributes.keys + params.require(:cert_issue_request).permit(attrs) end end diff --git a/app/controllers/info_controller.rb b/app/controllers/info_controller.rb new file mode 100644 index 0000000..1759a8c --- /dev/null +++ b/app/controllers/info_controller.rb @@ -0,0 +1,4 @@ +class InfoController < ApplicationController + def index + end +end diff --git a/app/lib/services/vault_service.rb b/app/lib/services/vault_service.rb index efb2381..d6e77f8 100644 --- a/app/lib/services/vault_service.rb +++ b/app/lib/services/vault_service.rb @@ -9,13 +9,10 @@ def initialize end def issue_cert(cert_issue_request) + opts = cert_issue_request.attributes # Generate the TLS certificate using the intermediate CA - tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], - common_name: cert_issue_request.common_name, - ttl: cert_issue_request.ttl, - ip_sans: cert_issue_request.ip_sans, - format: cert_issue_request.format) - tls_cert.data + tls_cert = @client.logical.write(Rails.application.config.astral[:vault_cert_path], opts) + OpenStruct.new tls_cert.data end end end diff --git a/app/models/cert_issue_request.rb b/app/models/cert_issue_request.rb index 6496f93..6863b42 100644 --- a/app/models/cert_issue_request.rb +++ b/app/models/cert_issue_request.rb @@ -3,16 +3,16 @@ class CertIssueRequest include ActiveModel::Attributes attribute :common_name, :string - attribute :alt_names, array: :string, default: [] + attribute :alt_names, :string attribute :exclude_cn_from_sans, :boolean, default: false attribute :format, :string, default: "pem" attribute :not_after, :datetime - attribute :other_sans, array: :string, default: [] + attribute :other_sans, :string attribute :private_key_format, :string, default: "pem" attribute :remove_roots_from_chain, :boolean, default: false attribute :ttl, :integer, default: Rails.configuration.astral[:cert_ttl] - attribute :uri_sans, array: :string, default: [] - attribute :ip_sans, array: :string, default: [] + attribute :uri_sans, :string + attribute :ip_sans, :string attribute :serial_number, :integer attribute :client_flag, :boolean, default: true attribute :code_signing_flag, :boolean, default: false @@ -29,15 +29,19 @@ class CertIssueRequest validate :validate_no_wildcards def fqdns - alt_names + [ common_name ] + alt_names_array + [ common_name ] end def validate_no_wildcards if common_name.present? errors.add(:common_name, "cannot be a wildcard") if common_name.start_with? "*" end - alt_names.each do |fqdn| + alt_names_array.each do |fqdn| errors.add(:alt_names, "cannot include a wildcard") if fqdn.start_with? "*" end end + + def alt_names_array + (alt_names || "").split(",") + end end diff --git a/app/views/certificates/create.json.jbuilder b/app/views/certificates/create.json.jbuilder new file mode 100644 index 0000000..589f1fa --- /dev/null +++ b/app/views/certificates/create.json.jbuilder @@ -0,0 +1,7 @@ +json.ca_chain @cert.ca_chain +json.certificate @cert.certificate +json.expiration @cert.expiration +json.issuing_ca @cert.issuing_ca +json.private_key @cert.private_key +json.private_key_type @cert.private_key_format +json.serial_number @cert.serial_number diff --git a/app/views/info/index.json.jbuilder b/app/views/info/index.json.jbuilder new file mode 100644 index 0000000..6d812c9 --- /dev/null +++ b/app/views/info/index.json.jbuilder @@ -0,0 +1,3 @@ +json.app "astral" +json.description "Astral provides a simplified API for PKI." +json.version "0.0.1" diff --git a/config/routes.rb b/config/routes.rb index 0de2bf3..726a700 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,7 +10,7 @@ get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # Defines the root path route ("/") - root "application#info" + root "info#index" resources :certificates, only: %i[create] end diff --git a/test/integration/certificates_controller_test.rb b/test/integration/certificates_controller_test.rb index b21e156..f952fa4 100644 --- a/test/integration/certificates_controller_test.rb +++ b/test/integration/certificates_controller_test.rb @@ -15,7 +15,7 @@ class CertificatesControllerTest < ActionDispatch::IntegrationTest test "#create authorized" do jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2huLmRvZUBleGFtcGxlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwiZ3JvdXBzIjpbImdyb3VwMSIsImdyb3VwMiJdLCJhdWQiOiJhc3RyYWwifQ.tfRLXmE_eq-piP88_clwPWrYfMAQbCJAeZQI6OFxZSI" post certificates_path, headers: { "Authorization" => "Bearer #{jwt}" }, - params: { common_name: "example.com" } + params: { cert_issue_request: { common_name: "example.com" } } assert_response :success %w[ ca_chain certificate diff --git a/test/models/cert_isssue_request_test.rb b/test/models/cert_isssue_request_test.rb index 5ab8022..85db25b 100644 --- a/test/models/cert_isssue_request_test.rb +++ b/test/models/cert_isssue_request_test.rb @@ -5,16 +5,16 @@ class CertIssueRequestTest < ActiveSupport::TestCase def setup @attributes = { common_name: "example.com", - alt_names: [ "alt1.example.com", "alt2.example.com" ], + alt_names: "alt1.example.com,alt2.example.com", exclude_cn_from_sans: true, format: "der", not_after: DateTime.now + 1.year, - other_sans: [ "other1", "other2" ], + other_sans: "other1,other2", private_key_format: "pkcs8", remove_roots_from_chain: true, ttl: 365, - uri_sans: [ "http://example.com" ], - ip_sans: [ "192.168.1.1" ], + uri_sans: "http://example.com", + ip_sans: "192.168.1.1", serial_number: 123456, client_flag: false, code_signing_flag: true, @@ -71,7 +71,7 @@ def setup end test "#valid? should prevent wildcard alt_names" do - @cert_issue_request.alt_names = [ "www.example.com", "*.example.com" ] + @cert_issue_request.alt_names = "www.example.com,*.example.com" assert_not @cert_issue_request.valid? assert_includes @cert_issue_request.errors[:alt_names], "cannot include a wildcard" end