diff --git a/README.md b/README.md index 57346f7..30a85ae 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,110 @@ Output render options are: certonly If set to true the x509 format will return only the certificate keyonly If set to true the x509 format will return only the private key +### cfssl + +This format will use [CFSSL](https://github.com/cloudflare/cfssl) to generate certificates and then sign it via remote CFSSL API server, for example: + +`trocla set testcert cfssl '{"CN" : "test.example.com","hosts":["test.example.com"],"names":[{"O":"Testorg","OU":"testcert"}],}'` + +Format for options is same config as CFSSL uses. That means all names must be in `hosts` key including CN. Plaintext pass is not used. + +Key type is set to RSA 2048 if not set in trocla call. `names` list can be set as default in trocla config or passed to trocla call to override default + +Required configuration: + +* cfssl installed in /usr/bin/cfssl (that's where Debian packages install it) or anywhere in PATH that trocla sees. +* cfssl CA configuration (example of it is in`lib/trocla/ca-config.json` +* cfssl config keys showing server URL and CA configuration + +Trocla config minimum setup: + +```yaml +formats: + cfssl: + server_url: https://certserver.example.com:8443 + cfssl_config_path: /etc/trocla/trocla-ca-config.json +``` + +#### Basic usage + +call trocla with hash describing cert to sign: +```json +{ + "CN": "*.example.com", + "hosts": [ + "*.example.com", + "example.com", + "10.0.0.1" + ], + "key": { + "algo": "rsa", + "size": 2048 + }, + "names": [ + { + "C": "AB", + "L": "Nowherecity", + "O": "Someorg", + "OU": "IT dept", + "ST": "nowhere", + "emailAddress": "admin@example.com" + } + ] +} +``` +and it will be signed using `server` cfssl profile + +`names` can be skipped if `default_names` key is specified in trocla config. `key` defaults to RSA 2048 and can be set globally via `default_key` key + +#### Additional generation options + +all other parameters are passed directly to cfssl + +##### profile + +Changes CFSSL profile. Defaults to 'server' + +##### selfsigned + +When set to true switches mode to generate selfsigned certs. Example: + +`trocla set testcerts cfssl '{"ca":{"expiry":"96h"},"selfsigned":true,"CN" : "test.example.com","hosts":["test.example.com"],"names":[{"O":"Testorg","OU":"testcert"}],}'` + +will generate selfsigned cert with lifetime 96 hours + +#### Additional trocla config options + +##### default_names + +Array to use if no `names` key is provided when requesting the certificate. For example: + +```yaml +default_names: + - C: AB + L: Nowherecity + O: Someorg + OU: IT dept + ST: nowhere + emailAddress: admin@example.com + +``` +#### intermediates + +Intermediates to add to the cert hash. Are **not** checked in any way + +#### Output + +Resulting hash will have those keys, in PEM format: + +* `cert` - cert itself +* `key` - key to the cert +* `csr` - CSR of the key +* `intermediates` - intermediates needed for cert to work +* `not_before` - stringified start date of cert ( 2019-02-28 13:31:00 UTC ) +* `not_after` - stringified end date of cert ( 2019-04-28 13:31:00 UTC ) + + ## Installation * Debian has trocla within its sid-release: `apt-get install trocla` diff --git a/lib/trocla/ca-config.json b/lib/trocla/ca-config.json new file mode 100644 index 0000000..35c1e52 --- /dev/null +++ b/lib/trocla/ca-config.json @@ -0,0 +1,46 @@ +{ + "signing": { + "default": { + "expiry": "12h" + }, + "profiles": { + "server": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "client-server": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + }, + "ca": { + "expiry": "87600h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 0, + "max_path_len_zero": true + }, + "usages": [ + "cert sign", + "crl sign" + ] + } + } + } +} diff --git a/lib/trocla/default_config.yaml b/lib/trocla/default_config.yaml index 78ea01f..7c7c37f 100644 --- a/lib/trocla/default_config.yaml +++ b/lib/trocla/default_config.yaml @@ -44,4 +44,12 @@ profiles: days: 2 # 1 day expires: 86400 - +formats: + cfssl: + server_url: http://localhost:8888 + #cfssl_config_path: ./ca-config.json + # intermediates: | + # -----BEGIN CERTIFICATE----- + # ... + # -----END CERTIFICATE----- + # diff --git a/lib/trocla/formats/cfssl.rb b/lib/trocla/formats/cfssl.rb new file mode 100644 index 0000000..699d5a3 --- /dev/null +++ b/lib/trocla/formats/cfssl.rb @@ -0,0 +1,72 @@ +class Trocla::Formats::Cfssl < Trocla::Formats::Base + require 'json' + require 'open3' + def format(plain_password,options={}) + #no dig method on jruby 1.9 used by puppet ;/ + if @trocla.config['formats'] && @trocla.config['formats']['cfssl'] + @cfssl_config = @trocla.config['formats']['cfssl'] + else + raise "cfssl format needs server parameters in formats -> cfssl config in the config file" + end + if !options.is_a?(Hash) + options = YAML.load(options) + end + selfsigned = false + if options['selfsigned'] + selfsigned = true + options.delete('selfsigned') + end + options['names'] ||= @cfssl_config['default_names'] + options['key'] ||= @cfssl_config['default_key'] || { 'algo' => 'rsa', 'size' => 2048 } + if selfsigned + options['profile'] ||= 'ca' + else + options['profile'] ||= 'server' + end + + + if plain_password.is_a?(Hash) && plain_password['cert'] && plain_password['key'] + # just an import, don't generate any new keys + # if cert does not have expiry info, add them (for certs imported manually) + if !plain_password['not_before'] + cert = OpenSSL::X509::Certificate.new plain_password['cert'] + plain_password['not_before'] = cert.not_before + plain_password['not_after'] = cert.not_after + end + return plain_password + end + @cfssl_config['cfssl_config_path'] ||= File.expand_path(File.join(File.dirname(__FILE__),'..','ca-config.json')) + if !options['CN'] || !options['names'] + raise "options passed should contain CN and names (if names are not defined in default config)" + end + # CA certs and client certs do not need to have list of hosts + if !options.key?('hosts') && !selfsigned + raise "options passed should contain hosts key with list of domains/IPs to sign. If you you really do not want hosts (client certs etc), pass hosts => false" + end + if options.key?('hosts') && !options['hosts'] + options.delete('hosts') + end + json_csr = JSON.dump(options) + if selfsigned + cfssl_cmd = ['cfssl','gencert','-initca=true','-config',@cfssl_config['cfssl_config_path'],'-profile', options['profile'], '-'] + else + cfssl_cmd = ['cfssl','gencert','-config',@cfssl_config['cfssl_config_path'],'-profile', options['profile'], '-remote',@cfssl_config['server_url'],'-'] + end + cfssl_stdout,cfssl_stderr = Open3.capture3( + *cfssl_cmd, + :stdin_data=>json_csr + ) + certdata = JSON.load(cfssl_stdout) + if !certdata.is_a?(Hash) || !certdata['cert'] + raise "cfssl: did not get cert data from server: stdin: #{json_csr} -> stdout: #{cfssl_stdout}, stderr #{cfssl_stderr}, config:#{@cfssl_config['cfssl_config_path']}" + end + if @cfssl_config['intermediates'] || options['intermediates'] + certdata['intermediate'] ||= options['intermediates'] || @cfssl_config['intermediates'] + end + # parse cert and extract validity date + cert = OpenSSL::X509::Certificate.new certdata['cert'] + certdata['not_before'] = cert.not_before + certdata['not_after'] = cert.not_after + return certdata + end +end