Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cfssl backend #54

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]"
}
]
}
```
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: [email protected]

```
#### 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`
Expand Down
46 changes: 46 additions & 0 deletions lib/trocla/ca-config.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
}
10 changes: 9 additions & 1 deletion lib/trocla/default_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ profiles:
days: 2
# 1 day
expires: 86400

formats:
cfssl:
server_url: http://localhost:8888
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm should we define that at all? Or is thi the default place where your cfssl daemon might be running?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default config of the daemon is localhost on port 8888:

 cfssl serve --help
  ...
  -address=127.0.0.1: Address to bind
  -port=8888: Port to bind
  ...

so I used that as an example

#cfssl_config_path: ./ca-config.json
# intermediates: |
# -----BEGIN CERTIFICATE-----
# ...
# -----END CERTIFICATE-----
#
72 changes: 72 additions & 0 deletions lib/trocla/formats/cfssl.rb
Original file line number Diff line number Diff line change
@@ -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