Skip to content

Commit

Permalink
Fixes #23211 - Add PuppetCa TokenVerify provider
Browse files Browse the repository at this point in the history
  • Loading branch information
Julian Todt committed Jun 21, 2018
1 parent e325683 commit 5bf0c2b
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 0 deletions.
3 changes: 3 additions & 0 deletions bundler.d/puppetca_token_whitelisting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
group :puppetca_token_whitelisting do
gem 'jwt'
end
1 change: 1 addition & 0 deletions config/settings.d/puppetca.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# valid providers:
# - puppetca_hostname_whitelisting (verify CSRs based on a hostname whitelist)
# - puppetca_token_whitelisting (verify CSRs based on a token whitelist)
#:use_provider: puppetca_hostname_whitelisting
#:ssldir: /var/lib/puppet/ssl
#:puppetca_use_sudo: true
Expand Down
7 changes: 7 additions & 0 deletions config/settings.d/puppetca_token_whitelisting.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
#
# Configuration of the PuppetCA token_whitelisting provider
#

#:sign_all: false
#:hosts_file: /tmp/foreman-proxy/hosts.yml
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module Proxy
require 'dhcp_libvirt/dhcp_libvirt'
require 'puppetca/puppetca'
require 'puppetca_hostname_whitelisting/puppetca_hostname_whitelisting'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting'
require 'puppet_proxy/puppet'
require 'puppet_proxy_customrun/puppet_proxy_customrun'
require 'puppet_proxy_legacy/puppet_proxy_legacy'
Expand Down
13 changes: 13 additions & 0 deletions modules/puppetca/puppetca_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ class Api < ::Sinatra::Base
end
end

post "/validate" do
content_type :json
unless autosigner.respond_to?('validate_csr')
log_halt 401, "Provider only supports trivial autosigning"
end
begin
request.body.rewind
autosigner.validate_csr(request.body.read) ? 200 : 404
rescue => e
log_halt 406, "Failed to validate CSR: #{e}"
end
end

delete "/autosign/:certname" do
content_type :json
certname = params[:certname]
Expand Down
13 changes: 13 additions & 0 deletions modules/puppetca_token_whitelisting/plugin_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module ::Proxy::PuppetCa::TokenWhitelisting
class PluginConfiguration
def load_classes
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_autosigner'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_csr'
end

def load_dependency_injection_wirings(container_instance, settings)
container_instance.dependency :autosigner, lambda { ::Proxy::PuppetCa::TokenWhitelisting::Autosigner.new }
end
end
end

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'puppetca_token_whitelisting/plugin_configuration'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_plugin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'jwt'
require 'openssl'

module ::Proxy::PuppetCa::TokenWhitelisting
class Autosigner
include ::Proxy::Log
include ::Proxy::Util

def sign_all
Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.sign_all
end

def hosts_file
Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.hosts_file
end

def smartproxy_cert
OpenSSL::PKey::RSA.new File.read Proxy::SETTINGS.ssl_private_key.to_s
end

def ensure_hostsfile
unless File.exist?(hosts_file)
FileUtils.mkdir_p hosts_file.rpartition('/').first
FileUtils.touch hosts_file
File.write(hosts_file, [].to_yaml)
end
end

# Invalidate a token based on the certname
def disable certname
hosts = autosign_list
hosts.delete(certname)
File.write hosts_file, hosts.to_yaml
end

# Create a new token for a certname
def autosign certname
ensure_hostsfile
payload = { certname: certname, exp: Time.now.to_i + 10 * 60 * 60 }
token = JWT.encode payload, smartproxy_cert, 'RS512'
File.write hosts_file, autosign_list.push(certname).to_yaml
{ generated_token: token }.to_json
end

# List the hosts that are currently valid
def autosign_list
ensure_hostsfile
YAML.load_file(hosts_file).to_a
end

# Check whether a csr is valid and should be signed
# by checking its token if it exists
def validate_csr csr
if csr.nil?
logger.warn "Request did not include a CSR."
return false
end
if sign_all
logger.warn "Signing CSR without token verification."
return true
end
begin
req = Proxy::PuppetCa::TokenWhitelisting::CSR.new csr
token = req.challenge_password
rescue
logger.warn "Invalid CSR"
return false
end
if token.nil?
logger.warn "CSR did not include a token."
return false
end
validate_token token
end

def validate_token token
# token didnt expire?
begin
decoded = JWT.decode(token, smartproxy_cert.public_key, true, algorithm: 'RS512')
rescue JWT::ExpiredSignature
logger.warn "Token already expired."
return false
rescue JWT::DecodeError
logger.warn "Failed to decode token."
return false
end
# token in our list?
unless autosign_list.include? decoded.first['certname']
logger.warn "Certname not valid."
return false
end
disable decoded.first['certname']
return true
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Proxy::PuppetCa::TokenWhitelisting
class CSR
attr_reader :csr

def initialize(raw_csr)
@csr = OpenSSL::X509::Request.new(raw_csr)
end

def challenge_password
attribute = custom_attributes.detect do |attr|
['challengePassword', '1.2.840.113549.1.9.7'].include?(attr[:oid])
end
attribute ? attribute[:value] : nil
end

def custom_attributes
@csr.attributes.map do |attr|
{
oid: attr.oid,
value: attr.value.value.first.value
}
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module ::Proxy::PuppetCa::TokenWhitelisting
class Plugin < ::Proxy::Provider
plugin :puppetca_token_whitelisting, ::Proxy::VERSION

requires :puppetca, ::Proxy::VERSION
default_settings :sign_all => false, :hosts_file => '/tmp/foreman-proxy/hosts.yml'

load_classes ::Proxy::PuppetCa::TokenWhitelisting::PluginConfiguration
load_dependency_injection_wirings ::Proxy::PuppetCa::TokenWhitelisting::PluginConfiguration
end
end
26 changes: 26 additions & 0 deletions test/fixtures/puppetca/csr_example.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEdTCCAl0CAQAwGzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAK+eSI5LYrBUROs02A9DYPqEpnN1YokD
MSb47NQW0A4+o7B7h/B3HFN2Moo/US/zNLDvHGuVDZIaIpudtNBTBSX0MQPtF8tQ
Lm7WA6wGwqpX3eR3OLXdWR/T4wIvFQ1gvCS/snuW8YennSeAl1Yijtr2EHiPFJ/i
Dy67vGhvXrqVDl/svIf13uw9zcxZ34VfE8zYg5WZ/thlhjbue/KKJdn+riNIYNK4
gwEjFNER9U38UPZUXMNJCEAXEJ7GKXQUKMXXtybp5jihPxTjbxFKilaIEJNM35ej
9Ra0OREGc0Cc0beaW9+n9ZFBxsfM/NV0nzOos4tmSEyAq54Nbd80ropYivrmSRQf
/yDPijvmC4frL1nhSJaf17rmfw27urbPcObbpuQFXCIlNFJd/CwpPds6ikZ9otDx
yTzPveLbQUCh3CzReODC7jopi7vchPH3cZVvuaE90REKL++3xHqOzRpq5tZ1xc2j
3agiQmTthnzABx6cj/q2ab4YAwpaSIAZFoHtzD8tsyD1WTGkL9jzBkCULd7ZcQdR
AL0PwGLYI2hbCsQV6i9noVZZ19+hHEjjk06lG5SKK+H8eTIybWr9IPU/yZowNPtd
et6samT8ltBLAxqTFffrdsTUTQAoN+ykgsML/N3LUuPACg1G6ge0MQVVvBIfCNiP
SjfPJmthHHc1AgMBAAGgFTATBgkqhkiG9w0BCQcxBhMEMTIzNDANBgkqhkiG9w0B
AQsFAAOCAgEAfTtWZAP8z9pxR6esLkoCfhhXaYzjnzJ5/4r+x/VPpJQEzI37CScG
Dma+UzVkIddCVc5oFtzLtVZtGTaygW2QyR7wu+an0qQBs+MVzkjPPnMLerDUR89c
Nk5DlDMaKFYV3JJpg2G2YdmOR0SEF0640Aw0/Ftx41iTSLSFopDMcTPRbZ9zm2AN
uer2TMIWpio6k6OyEJHkyifQOG1kgI+amVgk21kVRm5qUZV01QduLSxuN8KQKDYT
dmE31BpSQdVYzvrRPV558+NiWSrRheQtLfCl4BUZsZjfgh7OXSiy/yCZ6Co7FeDX
WbVtlIeaFukt5fPD4VKBQVIP296ZiB4BFIDLUcq87DaWbjbO1owS/B89MbAM6Fjy
FHCE3x0nUev3LYrKrJGouHYXiyUNGQk1ilI60y1xc3+B3ErjzpszJB1uBRk1fsem
Np60VUVqNPD7GzxlE/acgEa2Xij1vl+g4yIjWGVv5Fokb4fO6K+n8iSEYKG4+nMr
+vjP7bTcW4GymYPH38TtQfzhXvFMODL8ehiy2xjndEfLMitrbP1vjiLrCZD6gIk3
alxj7nHJMXF83Rqg/7OhERMVmUGE+tDiRD95bMK/eYq4sj49zke6puf5r0MeW6Fv
jfuZ0r0kJmJF/r2FZEKuScl0uS4/RWUvgUdUFwpZ3i8KzJWJ6NDm7eY=
-----END CERTIFICATE REQUEST-----
3 changes: 3 additions & 0 deletions test/fixtures/puppetca/hosts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- foo.example.com
- test.bar.example.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'test_helper'
require 'tempfile'
require 'fileutils'
require 'openssl'
require 'yaml'
require 'json'

require 'puppetca/puppetca'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_autosigner'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_csr'

class PuppetCaTokenWhitelistingAutosignerTest < Test::Unit::TestCase
def setup
@file = Tempfile.new('autosign_test')
begin
## Setup
FileUtils.cp './test/fixtures/puppetca/hosts.yml', @file.path
rescue
@file.close
@file.unlink
@file = nil
end
@autosigner = Proxy::PuppetCa::TokenWhitelisting::Autosigner.new
@autosigner.stubs(:hosts_file).returns(@file.path)
rsa_cert = OpenSSL::PKey::RSA.generate 2048
@autosigner.stubs(:smartproxy_cert).returns(rsa_cert)
end

def teardown
@file.close
@file.unlink
end

def test_should_list_autosign_entries
assert_equal @autosigner.autosign_list, ['foo.example.com', 'test.bar.example.com']
end

def test_should_add_autosign_entry
@autosigner.autosign 'foobar.example.com'
assert_equal @autosigner.autosign_list, ['foo.example.com', 'test.bar.example.com', 'foobar.example.com']
end

def test_should_create_correct_token
response = @autosigner.autosign 'baz.example.com'
token = JSON.parse(response)['generated_token']
decoded = JWT.decode(token, @autosigner.smartproxy_cert.public_key, true, algorithm: 'RS512')
assert_equal decoded.first['certname'], 'baz.example.com'
assert 100 > (decoded.first['exp'] - Time.now.to_i - 10 * 60 * 60).abs
end

def test_should_remove_autosign_entry
@autosigner.disable 'foo.example.com'
assert_equal @autosigner.autosign_list, ['test.bar.example.com']
end

def test_should_validate_on_sign_all
@autosigner.stubs(:sign_all).returns(true)
assert_true @autosigner.validate_csr ''
end

def test_should_call_verification
csr_example = File.read './test/fixtures/puppetca/csr_example.pem'
@autosigner.expects(:validate_token).with('1234').returns(true)
assert_true @autosigner.validate_csr csr_example
end

def test_should_validate_a_correct_token
response = @autosigner.autosign 'signme.example.com'
token = JSON.parse(response)['generated_token']

assert_true @autosigner.validate_token token
end

def test_should_not_validate_expired_token
payload = { certname: 'foo.example.com', exp: Time.now.to_i - 10 }
token = JWT.encode payload, @autosigner.smartproxy_cert, 'RS512'

assert_false @autosigner.validate_token token
end

def test_should_not_validate_token_with_invalid_certname
payload = { certname: 'unknown.example.com', exp: Time.now.to_i + 999_999 }
token = JWT.encode payload, @autosigner.smartproxy_cert, 'RS512'

assert_false @autosigner.validate_token token
end

def test_should_not_validate_token_with_unkown_signature
unknown_cert = OpenSSL::PKey::RSA.generate 2048
payload = { certname: 'foo.example.com', exp: Time.now.to_i + 999_999 }
token = JWT.encode payload, unknown_cert, 'RS512'

assert_false @autosigner.validate_token token
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'test_helper'
require 'puppetca/puppetca'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_plugin'

class PuppetCATokenWhitelistingConfigTest < Test::Unit::TestCase
def test_omitted_settings_have_default_values
Proxy::PuppetCa::TokenWhitelisting::Plugin.load_test_settings({})
assert_equal false, Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.sign_all
assert_equal '/tmp/foreman-proxy/hosts.yml', Proxy::PuppetCa::TokenWhitelisting::Plugin.settings.hosts_file
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require 'test_helper'

require 'puppetca/puppetca'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting'
require 'puppetca_token_whitelisting/puppetca_token_whitelisting_csr'

class PuppetCaTokenWhitelistingCSRTest < Test::Unit::TestCase
def setup
@csr_example = File.read './test/fixtures/puppetca/csr_example.pem'
end

def test_should_extract_correct_attribute
req = Proxy::PuppetCa::TokenWhitelisting::CSR.new @csr_example
assert_equal '1234', req.challenge_password
end

def test_should_fail_on_invalid_csr
@csr_example.slice!(42...69)
assert_raise OpenSSL::X509::RequestError do
Proxy::PuppetCa::TokenWhitelisting::CSR.new @csr_example
end
end
end

0 comments on commit 5bf0c2b

Please sign in to comment.