Commit e99d8484 authored by paz's avatar paz
Browse files

Make the API available via TLS, authenticate via api_keys.

parent 417cfe9a
......@@ -6,23 +6,45 @@ ENV['RACK_ENV'] ||= 'production'
require 'sinatra/base'
require 'sinatra/json'
require 'sinatra/namespace'
require 'thin'
require_relative '../lib/schleuder.rb'
class Schleuderd < Sinatra::Application
# Don't allow binding to anything but localhost to bar communication over the
# network until ssl is implemented.
if settings.bind != 'localhost'
$stderr.puts "\nWarning: Schleuderd currently enforces binding to localhost only, until connections are mandatorily encrypted and authentication is fixed.\n\n"
set :bind, 'localhost'
end
TLS_CERT = Conf.api['tls_cert_file']
TLS_KEY = Conf.api['tls_key_file']
class Schleuderd < Sinatra::Base
register Sinatra::Namespace
configure do
set :server, :thin
set :port, Schleuder::Conf.api['port'] || 4443
set :bind, Schleuder::Conf.api['bind'] || '0.0.0.0'
if settings.development?
set :logging, Logger::DEBUG
else
set :logging, Logger::WARN
end
end
configure :development do
set :logging, Logger::DEBUG
# TODO: Move this into a method, call that in the configure block
@use_tls = Conf.api_use_tls?
if @use_tls
[TLS_CERT, TLS_KEY].each do |const|
if const.nil?
@use_tls = false
elsif ! File.readable?(const)
$stderr.puts "Error: '#{const}' is not a readable file."
exit 1
end
end
end
configure :production do
set :logging, Logger::WARN
if @use_tls
use Rack::Auth::Basic, "Schleuder API Daemon" do |username, key|
username == 'schleuder' && Conf.api_valid_api_keys.include?(key)
end
else
$stderr.puts "\nWarning: Without TLS Schleuderd enforces binding to localhost only!\nExecute `schleuder cert generate` and follow the instructions.\n\n"
set :bind, 'localhost'
end
before do
......@@ -147,7 +169,6 @@ end
trust_issues: key.trust
}
end
end
namespace '/lists' do
......@@ -332,6 +353,18 @@ end
end
end
def self.run!
super do |server|
if @use_tls == true
server.ssl = true
server.ssl_options = {
:cert_chain_file => TLS_CERT,
:private_key_file => TLS_KEY
}
end
end
end
# Run this class as application
run!
end
......@@ -23,3 +23,16 @@ database:
production:
adapter: 'sqlite3'
database: /var/schleuder/db.sqlite
# Note: The API-daemon will bind only to localhost if no TLS-cert+keys are available.
api:
port: 4443
bind: 0.0.0.0
tls_cert_file: /etc/schleuder/schleuder-certificate.pem
tls_key_file: /etc/schleuder/schleuder-private-key.pem
# List of api_keys to allow access to the API.
# Example:
# valid_api_keys:
# - abcdef...
# - zyxwvu...
valid_api_keys:
......@@ -6,7 +6,6 @@ require 'pathname'
require 'syslog/logger'
require 'logger'
require 'open3'
require 'yaml'
# Require mandatory libs. The database-layer-lib is required below.
require 'mail-gpg'
......
......@@ -2,21 +2,34 @@ require 'thor'
require 'yaml'
require 'gpgme'
require_relative '../schleuder'
require 'schleuder/cli/subcommand_fix'
require 'schleuder/cli/schleuder_cert_manager'
require 'schleuder/cli/cert'
module Schleuder
class Cli < Thor
register(Cert,
'cert',
'cert ...',
'Generate TLS-certificate and show fingerprint')
map '-v' => :version
map '--version' => :version
desc 'version', 'Show version of schleuder'
def version
require_relative '../schleuder'
say Schleuder::VERSION
end
desc 'new_api_key', 'Generate a new API key to be used by a client.'
def new_api_key
require 'securerandom'
puts SecureRandom.hex(32)
end
desc 'work list@hostname < message', 'Run a message through a list.'
def work(listname)
require_relative '../schleuder'
message = STDIN.read
error = Schleuder::Runner.new.run(message, listname)
......@@ -38,8 +51,6 @@ module Schleuder
desc 'check_keys', 'Check all lists for unusable or expiring keys and send the results to the list-admins. (This is supposed to be run from cron weekly.)'
def check_keys(listname=nil)
require_relative '../schleuder'
Schleuder::List.all.each do |list|
I18n.locale = list.language
......@@ -54,7 +65,6 @@ module Schleuder
desc 'install', "Set-up or update Schleuder environment (create folders, copy files, fill the database)."
def install
require_relative '../schleuder'
%w[/var/schleuder/lists /etc/schleuder].each do |dir|
dir = Pathname.new(dir)
if ! dir.exist?
......@@ -91,8 +101,6 @@ module Schleuder
desc 'migrate-v2-list /path/to/listdir', 'Migrate list from v2.2 to v3.'
def migrate_v2_list(path)
require_relative '../schleuder'
dir = Pathname.new(path)
if ! dir.readable? || ! dir.directory?
fatal "Not a readable directory: `#{path}`."
......
module Schleuder
class Cert < Thor
extend SubcommandFix
desc 'generate', 'Generate a new TLS-certificate.'
def generate
# TODO: test if Conf.tls_key/cert_file are writeable, else use other filenames.
key = Conf.api['tls_key_file']
cert = Conf.api['tls_cert_file']
fingerprint = SchleuderCertGenerator.create('schleuder', key, cert)
say "Certificate written to: #{cert}"
say "Private key written to: #{key}"
say "If you move these files change tls_cert_file and tls_key_file in the configuration file accordingly."
#say "\nFingerprint of certificate: #{fingerprint}"
#say "Have this fingerprint included into the configuration-file of all clients (SchleuderConf, Webschleuder) that want to connect to your instance of schleuderd."
#fingerprint(cert)
end
desc 'fingerprint', 'Show fingerprint of configured certificate.'
def fingerprint
cert = Conf.api['tls_cert_file']
fingerprint = SchleuderCertManager.fingerprint(cert)
say "Fingerprint of #{Conf.api['tls_cert_file']}: #{fingerprint}"
end
end
end
require 'openssl'
require 'pathname'
class SchleuderCertManager
def self.generate(project_name, filename_key, filename_cert)
keysize = 2048
subject = "/C=MW/O=Schleuder/OU=#{project_name}"
filename_key = Pathname.new(filename_key).expand_path
filename_cert = Pathname.new(filename_cert).expand_path
key = OpenSSL::PKey::RSA.new(keysize)
cert = OpenSSL::X509::Certificate.new
cert.subject = OpenSSL::X509::Name.parse(subject)
cert.issuer = cert.subject
cert.not_before = Time.now
cert.not_after = Time.now + 10 * 365 * 24 * 60 * 60
cert.public_key = key.public_key
cert.serial = 0x0
cert.version = 2
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.extensions = [
ef.create_extension("basicConstraints","CA:TRUE", true),
ef.create_extension("subjectKeyIdentifier", "hash"),
]
cert.add_extension ef.create_extension("authorityKeyIdentifier",
"keyid:always,issuer:always")
cert.sign key, OpenSSL::Digest::SHA256.new
[filename_key, filename_cert].each do |filename|
if filename.exist?
error "File exists: #{filename} — (re)move it or change configuration file to proceed."
end
if ! filename.dirname.exist?
filename.dirname.mkpath
end
end
filename_key.open('w', 400) do |fd|
fd.puts key
end
filename_cert.open('w') do |fd|
fd.puts cert.to_pem
end
fingerprint(cert)
rescue => exc
error exc.message
end
def self.fingerprint(cert)
if ! cert.is_a?(OpenSSL::X509::Certificate)
path = Pathname.new(cert).expand_path
if ! path.readable?
error "Error: Not a readable file: #{path}"
end
cert = OpenSSL::X509::Certificate.new(path.read)
end
OpenSSL::Digest::SHA256.new(cert.to_der).to_s
end
def self.error(msg)
$stderr.puts "Error: #{msg}"
exit 1
end
end
module Schleuder
module SubcommandFix
# Fixing a bug in Thor where the actual subcommand wouldn't show up
# with some invokations of the help-output.
def banner(task, namespace = true, subcommand = true)
"#{basename} #{task.formatted_usage(self, true, subcommand).split(':').join(' ')}"
end
end
end
......@@ -36,6 +36,18 @@ module Schleuder
instance.config['log_level'] || 'WARN'
end
def self.api
instance.config['api'] || {}
end
def self.api_use_tls?
api['use_tls'].to_s == 'true'
end
def self.api_valid_api_keys
Array(api['valid_api_keys'])
end
# Three legacy options
def self.smtp_host
instance.config['smtp_host']
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment