From 8e52c850cedb3042a56557706bf70c7658d5aa09 Mon Sep 17 00:00:00 2001 From: Pino de Candia <32303022+pinodeca@users.noreply.github.com> Date: Fri, 19 Jan 2018 16:56:26 -0600 Subject: [PATCH] Implemented certificate revocation. --- devstack/local.conf | 1 + files/user-cloud-config | 27 +++++++++++++++++++--- tatu/api/app.py | 6 ++--- tatu/api/models.py | 46 ++++++++++++++++++++++++++----------- tatu/castellano.py | 1 - tatu/db/models.py | 50 ++++++++++++++++++++++++++++++++++++++++- tatu/db/persistence.py | 1 - tatu/utils.py | 29 ++++++++++++++++++++++-- 8 files changed, 136 insertions(+), 25 deletions(-) diff --git a/devstack/local.conf b/devstack/local.conf index 1492f82..b028573 100644 --- a/devstack/local.conf +++ b/devstack/local.conf @@ -30,6 +30,7 @@ disable_service q-agt # We have to disable the neutron dhcp agent. DF does not use the dhcp agent. disable_service q-dhcp +ENABLE_AGING_APP=True Q_ENABLE_DRAGONFLOW_LOCAL_CONTROLLER=True DF_SELECTIVE_TOPO_DIST=False DF_REDIS_PUBSUB=True diff --git a/files/user-cloud-config b/files/user-cloud-config index 9f8cf0f..bd1b662 100644 --- a/files/user-cloud-config +++ b/files/user-cloud-config @@ -13,7 +13,6 @@ write_files: import json import requests import os - import subprocess import uuid def getVendordataFromConfigDrive(): path = '/mnt/config/openstack/latest/vendor_data2.json' @@ -77,16 +76,38 @@ write_files: f.write(hostcert['key-cert.pub']) # Write the authorized principals file os.mkdir('/etc/ssh/auth_principals') - with open('/etc/ssh/auth_principals/ubuntu', 'w') as f: + with open('/etc/ssh/auth_principals/root', 'w') as f: for p in principals: f.write(p + os.linesep) # Write the User CA public key file with open('/etc/ssh/ca_user.pub', 'w') as f: f.write(tatu['auth_pub_key_user']) print 'All tasks completed.' + - path: /root/manage-revoked_keys.py + permissions: '0700' + owner: root:root + content: | + import base64 + import json + import requests + import uuid + path = '/mnt/config/openstack/latest/meta_data.json' + with open(path, 'r') as f: + json_string = f.read() + metadata = json.loads(json_string) + auth_id = str(uuid.UUID(metadata['project_id'], version=4)) + response = requests.get(server + '/noauth/revokedkeys/' + auth_id) + assert response.status_code == 200 + body = json.loads(response.content) + assert 'revoked_keys_data' in body + with open('/etc/ssh/revoked-keys', 'w') as f: + f.write(base64.b64decode(crl_body['revoked_keys_data'])) runcmd: + - dnf install -y python python-requests - python /root/setup-ssh.py > /var/log/setup-ssh.log 2>&1 - sed -i -e '$aTrustedUserCAKeys /etc/ssh/ca_user.pub' /etc/ssh/sshd_config - sed -i -e '$aAuthorizedPrincipalsFile /etc/ssh/auth_principals/%u' /etc/ssh/sshd_config - sed -i -e '$aHostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub' /etc/ssh/sshd_config - - systemctl restart ssh + - python /root/manage-revoked-keys.py >> /var/log/setup-ssh.log 2>&1 + - sed -i -e '$aRevokedKeys /etc/ssh/revoked-keys' /etc/ssh/sshd_config + - systemctl restart sshd diff --git a/tatu/api/app.py b/tatu/api/app.py index deacdf4..e2b6da1 100644 --- a/tatu/api/app.py +++ b/tatu/api/app.py @@ -11,11 +11,8 @@ # under the License. import falcon -import os.path -from oslo_config import cfg from oslo_log import log as logging -import models -from tatu import config # sets up all required config +from tatu import models from tatu.db.persistence import SQLAlchemySessionManager LOG = logging.getLogger(__name__) @@ -31,6 +28,7 @@ def create_app(sa): api.add_route('/hostcerts/{host_id}/{fingerprint}', models.HostCert()) api.add_route('/hosttokens', models.Tokens()) api.add_route('/novavendordata', models.NovaVendorData()) + api.add_route('/revokeduserkeys/{auth_id}', models.RevokedUserKeys()) return api diff --git a/tatu/api/models.py b/tatu/api/models.py index 95c4b4f..018824c 100644 --- a/tatu/api/models.py +++ b/tatu/api/models.py @@ -124,6 +124,15 @@ class Authority(object): resp.body = json.dumps(body) resp.status = falcon.HTTP_OK +def _userAsDict(user): + return { + 'user_id': user.user_id, + 'fingerprint': user.fingerprint, + 'auth_id': user.auth_id, + 'key-cert.pub': user.cert, + 'revoked': user.revoked, + 'serial': user.serial, + } class UserCerts(object): @falcon.before(validate) @@ -146,12 +155,7 @@ class UserCerts(object): users = db.getUserCerts(self.session) items = [] for user in users: - items.append({ - 'user_id': user.user_id, - 'fingerprint': user.fingerprint, - 'auth_id': user.auth_id, - 'key-cert.pub': user.cert, - }) + items.append(_userAsDict(user)) body = {'users': items} resp.body = json.dumps(body) resp.status = falcon.HTTP_OK @@ -164,13 +168,7 @@ class UserCert(object): if user is None: resp.status = falcon.HTTP_NOT_FOUND return - body = { - 'user_id': user.user_id, - 'fingerprint': user.fingerprint, - 'auth_id': user.auth_id, - 'key-cert.pub': user.cert, - } - resp.body = json.dumps(body) + resp.body = json.dumps(_userAsDict(user)) resp.status = falcon.HTTP_OK @@ -295,3 +293,25 @@ class NovaVendorData(object): req.body['instance-id'], 22) add_srv_records(req.body['hostname'], req.body['project-id'], port_ip_tuples) + +class RevokedUserKeys(object): + @falcon.before(validate) + def on_get(self, req, resp, auth_id): + body = { + 'auth_id': auth_id, + 'encoding': 'base64', + 'revoked_keys_data': db.getRevokedKeysBase64(self.session, auth_id) + } + resp.body = json.dumps(body) + resp.status = falcon.HTTP_OK + + @falcon.before(validate) + def on_post(self, req, resp, auth_id): + db.revokeUserKey( + self.session, + auth_id, + serial=req.body.get('serial', None), + key=req.body.get('key_id', None), + cert=req.body.get('cert', None) + ) + resp.status = falcon.HTTP_OK diff --git a/tatu/castellano.py b/tatu/castellano.py index ac1365e..08f568b 100644 --- a/tatu/castellano.py +++ b/tatu/castellano.py @@ -14,7 +14,6 @@ from castellan.common.objects.passphrase import Passphrase from castellan.common.utils import credential_factory from castellan.key_manager import API from castellan.key_manager.key_manager import KeyManager -from oslo_config import cfg from oslo_log import log as logging from tatu.config import CONTEXT diff --git a/tatu/db/models.py b/tatu/db/models.py index 062acce..6f0f99f 100644 --- a/tatu/db/models.py +++ b/tatu/db/models.py @@ -19,7 +19,7 @@ from sqlalchemy.ext.declarative import declarative_base import sshpubkeys from tatu.castellano import get_secret, store_secret -from tatu.utils import generateCert, random_uuid +from tatu.utils import generateCert, revokedKeysBase64, random_uuid Base = declarative_base() @@ -71,6 +71,8 @@ class UserCert(Base): fingerprint = sa.Column(sa.String(60), primary_key=True) auth_id = sa.Column(sa.String(36), sa.ForeignKey('authorities.auth_id')) cert = sa.Column(sa.Text) + serial = sa.Column(sa.Integer, index=True, autoincrement=True) + revoked = sa.Column(sa.Boolean, default=False) def getUserCert(session, user_id, fingerprint): @@ -107,6 +109,52 @@ def createUserCert(session, user_id, auth_id, pub): return user +class RevokedKey(Base): + __tablename__ = 'revoked_keys' + + auth_id = sa.Column(sa.String(36), primary_key=True) + serial = sa.Column(sa.Integer, sa.ForeignKey("user_certs.serial"), + primary_key=True) + + +def getRevokedKeysBase64(session, auth_id): + auth = getAuthority(session, auth_id) + if auth is None: + raise falcon.HTTPNotFound( + description='No Authority found with that ID') + serials = [k.serial for k in session.query(RevokedKey).filter( + RevokedKey.auth_id == auth_id)] + return revokedKeysBase64(getAuthUserKey(auth), serials) + + +def revokeUserKey(session, auth_id, serial=None, key_id=None, cert=None): + ser = None + userCert = None + if serial is not None: + userCert = session.query(UserCert).filter( + UserCert.serial == serial).one() + if userCert is None: + raise falcon.HTTPBadRequest( + "Can't find the certificate for serial # {}".format(serial)) + if userCert.auth_id != auth_id: + raise falcon.HTTPBadRequest( + "Incorrect CA ID for serial # {}".format(serial)) + ser = serial + elif key is not None: + # TODO(pino): look up the UserCert by key id and get the serial number + pass + elif cert is not None: + # TODO(pino): Decode the cert, validate against UserCert and get serial + pass + + if ser is None or userCert is None: + raise falcon.HTTPBadRequest("Cannot identify which Cert to revoke.") + + userCert.revoked = True + session.add(userCert) + session.add(RevokedKey(auth_id=auth_id, serial=ser)) + session.commit() + class Token(Base): __tablename__ = 'tokens' diff --git a/tatu/db/persistence.py b/tatu/db/persistence.py index b736e2c..9c867ab 100644 --- a/tatu/db/persistence.py +++ b/tatu/db/persistence.py @@ -10,7 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -import os from oslo_log import log as logging from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session diff --git a/tatu/utils.py b/tatu/utils.py index 86bb7cd..ede8a11 100644 --- a/tatu/utils.py +++ b/tatu/utils.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 import os import shutil import subprocess @@ -23,7 +24,6 @@ def random_uuid(): def generateCert(auth_key, entity_key, hostname=None, principals='root'): # Temporarily write the authority private key, entity public key to files - prefix = uuid.uuid4().hex temp_dir = mkdtemp() ca_file = '/'.join([temp_dir, 'ca_key']) pub_file = '/'.join([temp_dir, 'entity.pub']) @@ -48,4 +48,29 @@ def generateCert(auth_key, entity_key, hostname=None, principals='root'): cert = text_file.read() finally: shutil.rmtree(temp_dir) - return cert + return cert + + +def revokedKeysBase64(auth_key, serial_list): + # Temporarily write the authority private key and list of serials + temp_dir = mkdtemp() + ca_file = '/'.join([temp_dir, 'ca_key']) + serials_file = '/'.join([temp_dir, 'serials']) + revoked_file = '/'.join([temp_dir, 'revoked']) + try: + fd = os.open(ca_file, os.O_WRONLY | os.O_CREAT, 0o600) + os.close(fd) + with open(ca_file, "w") as text_file: + text_file.write(auth_key) + with open(serials_file, "w", 0o644) as text_file: + for s in serial_list: + text_file.write("serial: " + s + "\n") + args = ['ssh-keygen', '-s', ca_file, '-k', '-f', revoked_file, + serials_file] + subprocess.check_output(args, stderr=subprocess.STDOUT) + # Return the base64 encoded contents of the revoked keys file + with open(revoked_file, 'r') as text_file: + b64data = base64.b64encode(text_file.read()) + finally: + shutil.rmtree(temp_dir) + return b64data