-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add client certificate support to virtue-security
- Loading branch information
Patrick Dwyer
committed
Jan 4, 2018
1 parent
73ea932
commit 133b498
Showing
7 changed files
with
382 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,19 @@ | ||
FROM python:3.6 | ||
ENV PYTHONUNBUFFERED=0 | ||
|
||
# install virtue-security python dependencies | ||
WORKDIR /usr/src/app | ||
COPY requirements.txt ./ | ||
RUN pip install --no-cache-dir -r requirements.txt | ||
|
||
COPY gen_cert.sh ./ | ||
COPY tls_cert.conf ./ | ||
RUN ./gen_cert.sh | ||
|
||
# copy over our source and tools | ||
COPY . . | ||
|
||
CMD ["python", "./virtue-security"] | ||
# make sure we can run the bootstrap script | ||
RUN chmod ugo+x /usr/src/app/run.sh | ||
|
||
# install the gen certs modules | ||
RUN pip install -r ./tools/get_certificates_requirements.txt | ||
RUN mkdir /usr/src/app/certs | ||
|
||
CMD ["bash", "./run.sh"] |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/usr/bin/env bash | ||
|
||
echo "Getting Client Certificate" | ||
python /usr/src/app/tools/get_certificates.py --cfssl-host cfssl --hostname api -d /usr/src/app/certs --quiet | ||
|
||
echo "Running virtue-security" | ||
python /usr/src/app/virtue-security --public-key-path /usr/src/app/certs/cert.pem "$@" --ca-key-path /usr/src/app/certs/ca.pem --private-key-path /usr/src/app/certs/cert-key.pem |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
#!/usr/local/python | ||
""" | ||
Directly retrieve certificates from the certificate authority | ||
within Savior. This will only work for systems within the | ||
container network boundary of the Sensing API and CFSSL. | ||
This tool can be used to just retrieve the CA root public | ||
key, and/or to generate a new public/private key pair signed | ||
by the Savior CA. | ||
""" | ||
import argparse | ||
import base64 | ||
import datetime | ||
import hashlib | ||
import hmac | ||
import json | ||
import os | ||
import requests | ||
import socket | ||
import sys | ||
|
||
|
||
def get_ca_certificate(opts): | ||
""" | ||
Retrieve the CA root public certificate for the Savior network. | ||
The basic request looks like: | ||
curl -d '{"label": "primary"}' http://localhost:3030/api/v1/cfssl/info | ||
:param opts: argparse options | ||
:return: PEM encoded certificate as string | ||
""" | ||
log(opts, " %% Requesting CA public root certificate") | ||
|
||
res = requests.post("http://%s:%d/api/v1/cfssl/info" % (opts.cfssl_host, opts.cfssl_port), json={"label": "primary"}) | ||
|
||
if res.status_code == 200: | ||
log(opts, " * Got response from CA") | ||
payload = res.json() | ||
|
||
if payload["success"]: | ||
log(opts, " + Retrieved CA root key") | ||
ca_key = payload["result"]["certificate"] | ||
log(opts, " = %d bytes in key" % (len(ca_key),)) | ||
return ca_key | ||
else: | ||
print(" - Problem retrieving CA root key") | ||
for err in payload["result"]["errors"]: | ||
print(" ! %s" % (err,)) | ||
else: | ||
print(" ! Got an error from CA") | ||
print(" = status_code(%d)" % (res.status_code,)) | ||
|
||
return None | ||
|
||
|
||
def get_keys(opts): | ||
""" | ||
Retrieve a new public/private key pair from CFSSL. The basic requests | ||
look like: | ||
# new private key | ||
curl -d '{"key": {"algo": "rsa", "size": 4096}, "hosts":["sensor-001.savior"], "names":[{"C":"US", "ST":"Virginia", "L":"Arlington", "O":"sensor-001.savior"}], "CN": "sensor-001.savior"}' http://localhost:3030/api/v1/cfssl/newkey | ||
# CSR with HMAC | ||
curl -H "Accept: application/json" -H "Content-Type: application/json" -d '{"request": "...", "token": "dBJlaIkSKqY+IbWFwzeM7pVDTBc6n7If6t9Scr1SBDo="}' http://localhost:3030/api/v1/cfssl/authsign | ||
To get a key we walk through a few steps: | ||
1. Request a new private key | ||
2. Build a Certificate Signing Request | ||
3. Generate HMAC for the CSR | ||
4. Issue the CSR call to CFSSL | ||
:param opts: argparse options | ||
:return: (success, PEM public key, PEM private key) | ||
""" | ||
|
||
# structure our data for the private key request | ||
private_key_request = { | ||
"key": { | ||
"algo": opts.cert_algo, | ||
"size": opts.cert_size | ||
}, | ||
"hosts": [opts.hostname], | ||
"names": [ | ||
{ | ||
"C": "US", | ||
"ST": "Virginia", | ||
"L": "Arlington", | ||
"O": opts.hostname | ||
} | ||
], | ||
"CN": opts.hostname | ||
} | ||
|
||
priv_key_res = requests.post("http://%s:%d/api/v1/cfssl/newkey" % (opts.cfssl_host, opts.cfssl_port), json=private_key_request) | ||
|
||
# HTTP errors? | ||
if priv_key_res.status_code != 200: | ||
print(" ! Error retrieving new private key, status_code(%d)" % (priv_key_res.status_code,)) | ||
return False, None, None | ||
|
||
# let's pull apart the response JSON | ||
priv_key_json = priv_key_res.json() | ||
|
||
# Content/request errors? | ||
if not priv_key_json["success"]: | ||
print(" ! Private key request returned errors") | ||
for err in priv_key_json["result"]["errors"]: | ||
print(" - %s" % (err,)) | ||
|
||
return False, None, None | ||
|
||
log(opts, " + Private key retrieved") | ||
|
||
# save the private key and CSR for later use | ||
private_key = priv_key_json["result"]["private_key"] | ||
csr = priv_key_json["result"]["certificate_request"] | ||
|
||
# we need a signing request and a stringified version of the signing request | ||
signing_request = { | ||
"hostname": opts.hostname, | ||
"hosts": [opts.hostname], | ||
"certificate_request": csr, | ||
"profile": "default", | ||
"label": "client auth", | ||
"bundle": False | ||
} | ||
|
||
# this is the fun part. I swear I had to brute force every. single. combination. of these fields, | ||
# terms, base64ing's, and orderings, because the CFSSL documentation on this part is actively | ||
# hostile. | ||
signing_request_string = json.dumps(signing_request).encode("utf-8") | ||
|
||
# create an HMAC token for this request | ||
token_hmac = hmac.new(bytearray.fromhex(opts.cfssl_secret), signing_request_string, hashlib.sha256) | ||
token = base64.b64encode(token_hmac.digest()) | ||
|
||
# create the encoded signing request | ||
srs_b64 = base64.b64encode(signing_request_string) | ||
|
||
# setup the final request payload | ||
signing_payload = { | ||
"request": srs_b64.decode(), | ||
"token": token.decode() | ||
} | ||
# print("payload is: %s" % (json.dumps(signing_payload, indent=4))) | ||
# let's post it and see what happens | ||
signing_res = requests.post("http://%s:%d/api/v1/cfssl/authsign" % (opts.cfssl_host, opts.cfssl_port), json=signing_payload) | ||
|
||
if signing_res.status_code != 200: | ||
print(" ! Error retrieving signed key, status_code(%d)" % (signing_res.status_code)) | ||
return False, None, None | ||
|
||
signing_res_json = signing_res.json() | ||
|
||
if not signing_res_json["success"]: | ||
print(" ! Signing request returned errors") | ||
for err in signing_res_json["result"]["errors"]: | ||
print(" - %s" % (err,)) | ||
|
||
return False, None, None | ||
|
||
# ok, we probably have a good response! | ||
public_key = signing_res_json["result"]["certificate"] | ||
log(opts, " + Signed Public key retrieved") | ||
|
||
return True, public_key, private_key | ||
|
||
|
||
def have_shared_secret(opts): | ||
""" | ||
Check for the cfssl shared secret in our CLI options. If it hasn't been | ||
specified, check the environment variable CFSSL_SHARED_SECRET. If that | ||
doesn't exist, we've got a problem. | ||
:param opts: argparse options | ||
:return: True if we have a shared secret, otherwise false | ||
""" | ||
|
||
if opts.cfssl_secret is None: | ||
|
||
# is it in the ENV? | ||
if "CFSSL_SHARED_SECRET" in os.environ: | ||
opts.cfssl_secret = os.environ["CFSSL_SHARED_SECRET"] | ||
return True | ||
else: | ||
return False | ||
else: | ||
return True | ||
|
||
|
||
def have_hostname(opts): | ||
""" | ||
Check that we have a hostname for generating and using TLS certs. We'll | ||
default to the hostname bound by a local socket if one isn't specified | ||
on the CLI. | ||
:param opts: argparse options | ||
:return: True if we have a hostname, otherwise False | ||
""" | ||
|
||
if opts.hostname is None: | ||
opts.hostname = socket.gethostname() | ||
if opts.hostname is None: | ||
return False | ||
else: | ||
return True | ||
else: | ||
return True | ||
|
||
|
||
def options(): | ||
""" | ||
Parse out the command line options. | ||
:return: | ||
""" | ||
parser = argparse.ArgumentParser(description="Retrieve TLS certificates from the Savior CA") | ||
|
||
# top level control | ||
|
||
# communications | ||
parser.add_argument("--cfssl-host", dest="cfssl_host", default="cfssl", help="CFSSL hostname") | ||
parser.add_argument("--cfssl-port", dest="cfssl_port", default=3030, type=int, help="CFSSL port") | ||
|
||
# CA only mode | ||
parser.add_argument("--ca-only", dest="ca_only", default=False, action="store_true", help="Only retrieve the CA public root") | ||
|
||
# host for certificate - we'll check this later and default to the socket hostname | ||
parser.add_argument("--hostname", dest="hostname", default=None, help="Hostname to issue certificate for") | ||
|
||
# directory for certificates | ||
parser.add_argument("-d", "--certificate-directory", dest="cert_dir", default="./certs", help="Directory in which to place certificates") | ||
|
||
# certificate control | ||
# algo | ||
# size | ||
parser.add_argument("--certificate-algorithm", dest="cert_algo", default="rsa", help="Certificate algorithm") | ||
parser.add_argument("--certificate-size", dest="cert_size", default=4096, type=int, help="Certificate bit size") | ||
|
||
# check for our shared key for signing requests. If we don't have this on the CLI, we'll check | ||
# the ENV as well. Barring that, we'll bail. This is all handled in a separate method (see have_shared_secret | ||
# method) that is only run if we're actually requesting new keys | ||
parser.add_argument("--cfssl-shared-secret", dest="cfssl_secret", default=None, help="Shared secret used to generate HMAC verified requests to CFSSL. Will default to environment variable CFSSL_SHARED_SECRET if not specific") | ||
|
||
parser.add_argument("-q", "--quiet", dest="quiet", default=False, action="store_true", help="Run in quiet mode, which reduces the command logging output") | ||
|
||
return parser.parse_args() | ||
|
||
|
||
def log(opts, msg): | ||
if not opts.quiet: | ||
print(msg) | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
opts = options() | ||
|
||
# make sure our directory exists for key storage | ||
cert_path = os.path.realpath(opts.cert_dir) | ||
if not os.path.exists(cert_path): | ||
log(opts, " @ Certificate directory doesn't yet exist - creating") | ||
os.makedirs(cert_path) | ||
|
||
# track which keys we'll write out | ||
keys_to_write = {} | ||
|
||
# time tracking | ||
st = datetime.datetime.now() | ||
|
||
# retrieve the ca certificate | ||
ca_key = get_ca_certificate(opts) | ||
if ca_key is None: | ||
sys.exit(1) | ||
keys_to_write["ca.pem"] = ca_key | ||
|
||
# retrieve a new pub/priv key pair | ||
if not opts.ca_only: | ||
|
||
# gotta have a shared secret | ||
if not have_shared_secret(opts): | ||
print(" ! No CFSSL shared secret available - cannot generate a CSR") | ||
sys.exit(1) | ||
|
||
if not have_hostname(opts): | ||
print(" ! No hostname found - cannot generate a CSR") | ||
sys.exit(1) | ||
|
||
# let's get us some keys | ||
success, pub_key, priv_key = get_keys(opts) | ||
|
||
if success: | ||
keys_to_write["cert.pem"] = pub_key | ||
keys_to_write["cert-key.pem"] = priv_key | ||
else: | ||
print(" ! There was an error generating a public/private key pair - aborting") | ||
sys.exit(1) | ||
|
||
# write out our keys | ||
for filename, key_data in keys_to_write.items(): | ||
log(opts, " > writing [%s] to [%s]" % (filename, os.path.join(opts.cert_dir, filename))) | ||
with open(os.path.join(cert_path, filename), "w") as keyfile: | ||
keyfile.write(key_data) | ||
log(opts, " % setting permissions to 0x600") | ||
os.chmod(os.path.join(cert_path, filename), 0o600) | ||
|
||
ed = datetime.datetime.now() | ||
log(opts, " ~ completed CA requests in %s" % (str(ed - st),)) |
5 changes: 5 additions & 0 deletions
5
control/virtue-security/tools/get_certificates_requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
certifi==2017.11.5 | ||
chardet==3.0.4 | ||
idna==2.6 | ||
requests==2.18.4 | ||
urllib3==1.22 |
Oops, something went wrong.