├── LICENCE ├── README.md └── yubikey-ca /LICENCE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unmaintained Project 2 | 3 | I am not using, even less maintaining this project anymore. 4 | 5 | # yubikey-ca 6 | 7 | ## Overview 8 | 9 | `yubikey-ca` is a simple command line tool used to build a PKI and SSH 10 | CA, powered by a Yubikey (or other PKCS#11 tokens) for private key 11 | management and git for history. 12 | 13 | Every operation that change the state of the CA (such as emitting or 14 | revoking certificates) creates a git commit ; you can for example `git 15 | revert` a revoke operation. 16 | 17 | ## Dependencies 18 | 19 | * Python >= 3.2 20 | * OpenSSL 21 | * [OpenSSL PKCS#11 engine from OpenSC](https://github.com/OpenSC/libp11/) 22 | * `p11tool` from GnuTLS 23 | * A PKCS#11 provider; for a Yubikey, you can use OpenSC 24 | * SSH (for the SSH CA, not required for the SSL PKI) 25 | 26 | ## PIN 27 | 28 | The default PIN used to access the Yubikey is 123456. You can change 29 | it with the `PIN` environment variable ; the special value `ask` will 30 | issue a prompt if the PIN is needed. 31 | 32 | ## Initialization 33 | 34 | `yubikey-ca init-ca` will initialize a new CA in the current directory 35 | (you probably want an empty one). It will create a new git repository, 36 | files needed by the CA, and do a first commit. 37 | 38 | `yubikey-ca init-key` will initialize the private key on the Yubikey. You 39 | have three possibilities : 40 | 41 | * On-device key generation: `yubikey-ca init-key -g`. This 42 | is the recommended method, unless your device is vulnerable to 43 | [this security advisory](https://www.yubico.com/support/security-advisories/ysa-2017-01/). 44 | * Software key generation: `openssl genrsa 2048 | yubikey-ca init-key`. This 45 | is the recommened method if your device is affected by the previous issue. 46 | * Using an existing key, in PEM format: `yubikey-ca init-key -f key.pem`. 47 | 48 | You should also give a name to your CA, the default being "Yubikey 49 | CA". You can do so with the `-s` option : 50 | 51 | * `yubikey-ca init-key -g -s "/O=Example Inc/CN=Certificate Authority"` 52 | 53 | Now that your CA key is on the device, you can access to your CA 54 | certificate: 55 | 56 | * `yubikey-ca ca-cert` 57 | 58 | or to your SSH CA certificate: 59 | 60 | * `yubikey-ca ssh-ca` 61 | 62 | ## Using the PKI 63 | 64 | ### Issuing and revoking certificates 65 | 66 | Generating a certificate: 67 | 68 | * `openssl genrsa 2048 | yubikey-ca client-cert` 69 | 70 | You must give a name to the certificate, for example: 71 | 72 | * `yubikey-ca client-cert -s "/O=Example Inc/CN=John Doe"` 73 | 74 | You can use other algorithms for your client certificates, for example 75 | `openssl genrsa 4096` for a different key size or `openssl ecparam 76 | -genkey -name secp256k1` for an ECC key. 77 | 78 | The `-e` option can be used to export the certificate + key in the PKCS#12 79 | format for importing it in the browser. 80 | 81 | The certificates are stored in the `certs/` directory, and are listed 82 | in the `index.txt` file. The first field describe the certificate status 83 | (`V` for valid), the second the issuing date, the third the certificate 84 | serial and the last one the certificate name. 85 | 86 | To revoke certificates, you must provide their serial: 87 | 88 | * `yubikey-ca client-revoke 01 03` 89 | 90 | Generate a CRL containing the revoked certificates: 91 | 92 | * `yubikey-ca crl` 93 | 94 | ## Using the SSH CA 95 | 96 | ### Signing SSH certificates 97 | 98 | * `yubikey-ca ssh-cert id_rsa.pub` 99 | 100 | It will generate `id_rsa-cert.pub`, that can be used by the owner of the 101 | corresponding private key, either as-is, or with the `CertificateFile` 102 | option. 103 | 104 | Issued certificates are listed in `ssh-index.txt`. It contains the 105 | following fields: 106 | 107 | * First field: validity, V for valid and R for revoked 108 | * `serial`: identifier for revokation 109 | * `pub`: public key file, base64 encoded 110 | * `principals`: list of principals for the certificate. Defaults to empty, 111 | can be configured with `-n`. 112 | * `id`: id of the certificate, used in logs. Defaults to the comment 113 | field of the provided SSH public key, can be configured with `-I`. 114 | 115 | Revoking certificates is done by providing serials: 116 | 117 | * `yubikey-ca ssh-revoke 16388656242419284907 14560815252314548972` 118 | 119 | You can the generate a KRL containing the revoked certificates : 120 | 121 | * `yubikey-ca krl` 122 | 123 | ### Principals 124 | 125 | SSH certificates grants access to *principals*. The server possess a list 126 | of principals that can be used to grant access, the client certificate 127 | possess a list of valid principals. A client is allowed to authenticate 128 | if the intersection of the server-side principals and the client-side 129 | principals (from your certificates) is not empty. 130 | 131 | For example, if the server-side principals for `website@staging-host` 132 | are `env:staging` and `type:wordpress`, you need either `env:staging` 133 | or `type:wordpress` in the principals list in your certificate for 134 | successful authentication. 135 | 136 | By default, the server-side principals list only contains the target user 137 | (for `website@staging-host`, the list would contain a single principal, 138 | `website`). 139 | 140 | ### Server-side configuration 141 | 142 | In `/etc/ssh/sshd_config`: 143 | 144 | * `TrustedUserCAKeys /etc/ssh/authorized_ca` where `authorized_ca` 145 | contains the result of `yubikey-ca ssh-ca`. You can have multiple entries. 146 | * `RevokedKeys /etc/ssh/revoked_keys` where `revoked_keys` contains the 147 | result of `yubikey-ca krl`. **Warning**: Be sure to have an empty KRL 148 | here initially, because if the file does not exists all keys will be 149 | rejected after restarting `sshd` 150 | 151 | Suggested configuration: 152 | 153 | * `AuthorizedPrincipalsCommandUser nobody` and 154 | * `AuthorizedPrincipalsCommand /bin/sh -c "printf 'u:%u\nu:%u@host\nh:host\n*\n'"` 155 | 156 | (replace host by the name of your server) 157 | 158 | This add some useful default principals: 159 | 160 | * `*`, so a certificate containing the `*` principal will be allowed to 161 | login anywhere. 162 | * `u:%u`, to allow creation of "an user on all hosts" certificates. 163 | * `u:%u@host` to allow creation of "specific user on specific host" certificates. 164 | * `h:host`, to allow creation of "all users on a host" certificates. 165 | 166 | You can add "thematic" server-wide principals here, like `env:staging` 167 | or `OU:qa`, or the roles or your server from your Ansible/Puppet/Chef 168 | setup. 169 | 170 | * `AuthorizedPrincipalsFile .ssh/authorized_principals` 171 | 172 | This allows you to configure a per-user list of principals in its home 173 | directory, similarly to `.ssh/authorized_keys` (one principal per line, 174 | not comma-separated). It does not override the server-wide list of 175 | principals but adds to it. 176 | 177 | ## Known bugs 178 | 179 | * The `ssh-cert` subcommand always ask for your PIN, regardless of the 180 | PIN environment variable. 181 | 182 | ## Limitations 183 | 184 | To keep things simple, a lot of things that are configurable in a 185 | full-fledged OpenSSL-powered CA (like `easy-rsa`) are hardcoded. In 186 | particular (this is not an exhaustive list): 187 | 188 | * `init-key` will use RSA 2048 189 | * The CA certificate is valid for 10 years 190 | * The CA CRLs are valid for 10 years 191 | * The SSL certificates are valid for 1 year 192 | * The SSL certificates are created from a private key, not a CSR 193 | * The SSH certificates are valid forever 194 | * CA policy is extremely loose, just requiring a `CN` in the subject 195 | * Certificates purpose is hardcoded (SSL client and S/MIME) 196 | 197 | If you really need different values, patches are welcome, but as for 198 | now I have no plan to implement options for alternative values for every 199 | single thing "just in case someone needs them". 200 | 201 | ## Using another PKCS#11 token 202 | 203 | Althought yubikey-ca is primarily intended to be used with a Yubikey, 204 | it can be used with others PKCS#11 tokens: 205 | 206 | * If you must use another provider than the default (`opensc-pkcs11.so`), 207 | either use the `PKCS11_MODULE` environment variable or the `pkcs11-module` 208 | command-line option. 209 | * Specify the slot URL with `PKCS11_URL` environment 210 | variable or `pkcs11-url` option. You can get the list with `p11tool 211 | --provider=$PKCS11_MODULE --list-all` ; you can use only parts of the URL 212 | (for example, only `manufacturer` and `id`), you must remove the `type=` 213 | part of the URL, and you must keep the `id=` part of the URl. 214 | * `init-key` won't work, you will have to manually initialize the token. 215 | 216 | ### Example with SoftHSM 217 | 218 | Initialization (you may want to change the token name, `SoftHSM`, the 219 | CA subject, `Test CA`, and the PINs): 220 | 221 | ``` 222 | export SOFTHSM2_CONF=/path/to/softhsm2.conf 223 | TOKEN="SoftHSM" 224 | CA="/CN=Test CA" 225 | PIN="123456" 226 | SO_PIN="$PIN" 227 | 228 | softhsm2-util --init-token --slot 0 --label "$TOKEN" --pin "$PIN" --so-pin "$SO_PIN" 229 | KEY=$(openssl genrsa 2048) 230 | pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --id 02 --type privkey --label "SIGN privkey" --pin "$PIN" --write <(openssl rsa -in <(echo "$KEY") -outform der) 231 | pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --id 02 --type pubkey --label "SIGN pubkey" --pin "$PIN" --write <(openssl rsa -in <(echo "$KEY") -outform der -pubout) 232 | pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --id 02 --type cert --label "Certificate for Digital Signature" --pin "$PIN" --write <(openssl req -x509 -key <(echo "$KEY") -sha256 -days 3560 -subj "$CA" -outform der) 233 | ``` 234 | 235 | Example usage: 236 | 237 | ``` 238 | export SOFTHSM2_CONF=/path/to/softhsm2.conf 239 | 240 | yubikey-ca -m /usr/lib/softhsm/libsofthsm2.so -u "pkcs11:token=SoftHSM;id=%02" ca-cert 241 | PKCS11_MODULE="/usr/lib/softhsm/libsofthsm2.so" PKCS11_URL="pkcs11:token=SoftHSM;id=%02" yubikey-ca client-cert 242 | ``` 243 | -------------------------------------------------------------------------------- /yubikey-ca: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import argparse 4 | import base64 5 | import datetime 6 | import functools 7 | import getpass 8 | import hashlib 9 | import os 10 | import signal 11 | import subprocess 12 | import sys 13 | import threading 14 | 15 | @functools.lru_cache() 16 | def get_pin(): 17 | pin = os.getenv('PIN', '123456') 18 | if pin == 'ask': 19 | pin = getpass.getpass('PIN: ') 20 | return pin 21 | 22 | def get_ca_cert(): 23 | return run('p11tool', '--provider=%s' % pkcs11_module, '--export', '%s;type=cert' % args.pkcs11_url).strip()+b"\n" 24 | 25 | def get_ssh_ca(): 26 | pubkey = run('openssl', 'x509', '-noout', '-pubkey', input=get_ca_cert()) 27 | return run('ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin', input=pubkey) 28 | 29 | class Shell: 30 | def __init__(self): 31 | self.out = {} 32 | self.pass_fds = [] 33 | self.pipeout_fds = [] 34 | 35 | def pipein(self, data): 36 | """ 37 | Equivalent of zsh/bash <() operator 38 | Create a pipe, send data to it so a child process can read it as a /proc/self/fd/ file 39 | """ 40 | rfd, wfd = os.pipe() 41 | if os.fork() == 0: 42 | os.close(rfd) 43 | while data: 44 | data = data[os.write(wfd, data):] 45 | os.close(wfd) 46 | sys.exit(0) 47 | else: 48 | self.pass_fds.append(rfd) 49 | os.close(wfd) 50 | return "/proc/self/fd/%s" % rfd 51 | 52 | def pipeout(self, name): 53 | """ 54 | Provides a fd usable by the subprocess for writing 55 | Its result will be in sh.out[name] 56 | """ 57 | rfd, wfd = os.pipe() 58 | self.pass_fds.append(wfd) 59 | self.pipeout_fds.append((name, rfd)) 60 | return "/proc/self/fd/%s" % wfd 61 | 62 | def _read_pipe(self, name, rfd): 63 | with os.fdopen(rfd, 'rb') as f: 64 | self.out[name] = f.read() 65 | 66 | def _close_fds(self): 67 | for fd in self.pass_fds: 68 | os.close(fd) 69 | self.pass_fds.clear() 70 | 71 | def __enter__(self): 72 | return self 73 | 74 | def __exit__(self, exc_type, exc_value, traceback): 75 | self._close_fds() 76 | 77 | def run(self, *args, input=None, **kwargs): 78 | kwargs.setdefault('stdout', subprocess.PIPE) 79 | if input is not None: 80 | kwargs.setdefault('stdin', subprocess.PIPE) 81 | with subprocess.Popen(args, pass_fds=self.pass_fds, **kwargs) as process: 82 | self._close_fds() 83 | threads = [] 84 | for pfd in self.pipeout_fds: 85 | thread = threading.Thread(target=self._read_pipe, args=pfd) 86 | thread.start() 87 | threads.append(thread) 88 | stdout, stderr = process.communicate(input) 89 | retcode = process.wait() 90 | [thread.join() for thread in threads] 91 | if retcode: 92 | raise subprocess.CalledProcessError(retcode, process.args, output=stdout, stderr=stderr) 93 | return stdout 94 | 95 | def run(*args, **kwargs): 96 | with Shell() as sh: 97 | return sh.run(*args, **kwargs) 98 | 99 | def ca_commit(message): 100 | for f in ['index.txt.old', 'serial.old', 'index.txt.attr.old', 'crlnumber.old']: 101 | if os.path.exists(f): 102 | os.unlink(f) 103 | 104 | subprocess.check_call(['git', 'add', 'ssh-index.txt']) 105 | subprocess.check_call(['git', 'add', 'index.txt']) 106 | subprocess.check_call(['git', 'add', 'index.txt.attr']) 107 | subprocess.check_call(['git', 'add', 'crlnumber']) 108 | subprocess.check_call(['git', 'add', 'serial']) 109 | subprocess.check_call(['git', 'add', 'certs']) 110 | subprocess.check_output(['git', 'commit', '-m', message]) 111 | 112 | def openssl_ca(sh): 113 | return ('openssl', 'ca', '-config', sh.pipein(ca_config.encode()), '-engine', 'pkcs11', '-keyform', 'engine', '-batch', 114 | '-keyfile', args.pkcs11_url, '-passin', 'pass:%s' % get_pin(), '-cert', sh.pipein(get_ca_cert())) 115 | 116 | parser = argparse.ArgumentParser() 117 | subparsers = parser.add_subparsers(dest='action') 118 | 119 | parser.add_argument('--pkcs11-module', '-m', default=os.getenv('PKCS11_MODULE', 'opensc-pkcs11.so')) 120 | parser.add_argument('--pkcs11-url', '-u', default=os.getenv('PKCS11_URL', 'pkcs11:manufacturer=piv_II;id=%02')) 121 | parser.add_argument('--debug', action='store_true') 122 | 123 | subparsers.add_parser('ssh-ca') 124 | subparsers.add_parser('ca-cert') 125 | 126 | parser_init_key = subparsers.add_parser('init-key') 127 | parser_init_key.add_argument('--subject', '-s', default='/CN=Yubikey CA') 128 | parser_init_key.add_argument('--keyfile', '-k', default='/dev/stdin') 129 | parser_init_key.add_argument('--generate', '-g', action='store_true', help='Generate key on device') 130 | 131 | parser_init_ca = subparsers.add_parser('init-ca') 132 | 133 | parser_ssh_cert = subparsers.add_parser('ssh-cert') 134 | parser_ssh_cert.add_argument('--principals', '-n', help='comma-separated list of principals (see CERTIFICATES in ssh-keygen)') 135 | parser_ssh_cert.add_argument('--id', '-I') 136 | parser_ssh_cert.add_argument('--options', '-O', action='append', default=[]) 137 | parser_ssh_cert.add_argument('keyfile') 138 | 139 | parser_client_cert = subparsers.add_parser('client-cert') 140 | parser_client_cert.add_argument('--subject', '-s', required=True) 141 | parser_client_cert.add_argument('--keyfile', '-k', default='/dev/stdin') 142 | parser_client_cert.add_argument('--pkcs12', '-e', action='store_true') 143 | 144 | parser_client_revoke = subparsers.add_parser('client-revoke') 145 | parser_client_revoke.add_argument('serial', nargs='+') 146 | 147 | parser_client_revoke = subparsers.add_parser('ssh-revoke') 148 | parser_client_revoke.add_argument('serial', nargs='+') 149 | 150 | parser_crl = subparsers.add_parser('crl') 151 | parser_crl = subparsers.add_parser('krl') 152 | 153 | args = parser.parse_args() 154 | 155 | if args.debug: 156 | os.environ['PKCS11SPY'] = args.pkcs11_module 157 | pkcs11_module = 'pkcs11-spy.so' 158 | else: 159 | pkcs11_module = args.pkcs11_module 160 | 161 | ca_config = '\n'.join([ 162 | 'openssl_conf = openssl_init', 163 | '[ca]', 164 | 'default_ca = ca_section', 165 | '[ca_section]', 166 | 'database = ./index.txt', 167 | 'crlnumber = ./crlnumber', 168 | 'serial = ./serial', 169 | 'new_certs_dir = ./certs', 170 | 'crl = ./crl.pem', 171 | 'unique_subject = no', 172 | 'x509_extensions = usr_section', 173 | 'default_md = sha256', 174 | 'default_crl_days = 30', 175 | 'policy = policy_section', 176 | '[policy_section]', 177 | 'countryName = optional', 178 | 'stateOrProvinceName = optional', 179 | 'organizationName = optional', 180 | 'organizationalUnitName = optional', 181 | 'commonName = supplied', 182 | 'emailAddress = optional', 183 | '[usr_section]', 184 | 'basicConstraints=CA:FALSE', 185 | 'nsCertType = client, email', 186 | '[openssl_init]', 187 | 'engines = engine_section', 188 | '[engine_section]', 189 | 'pkcs11 = pkcs11_section', 190 | '[pkcs11_section]', 191 | 'engine_id = pkcs11', 192 | 'MODULE_PATH = %s' % pkcs11_module, 193 | 'init = 0', 194 | (args.debug and 'VERBOSE = EMPTY' or ''), 195 | ]) 196 | 197 | if args.action == 'init-key': 198 | if args.generate: 199 | pubkey = run('yubico-piv-tool', '-s9c', '-agenerate', '-ARSA2048') 200 | # Generate a temporary self-signed certificate in order to make openssl happy 201 | with Shell() as sh: 202 | cert = sh.run('yubico-piv-tool', '-s9c', '-averify', '-P%s' % get_pin(), '-aselfsign', '-i%s' % sh.pipein(pubkey), '-S/CN=TempCA') 203 | with Shell() as sh: 204 | sh.run('yubico-piv-tool', '-s9c', '-aimport-certificate', '-i%s' % sh.pipein(cert)) 205 | # Selfsign with openssl now, in order to have CA in purposes 206 | with Shell() as sh: 207 | cert = sh.run('openssl', 'req', '-x509', '-days', '3650', '-sha256', '-subj', args.subject, '-engine', 'pkcs11', 208 | '-keyform', 'engine', '-key', args.pkcs11_url, '-passin', 'pass:%s' % get_pin()) 209 | else: 210 | key = open(args.keyfile, 'rb').read() 211 | with Shell() as sh: 212 | sh.run('yubico-piv-tool', '-s9c', '-aimport-key', '-i', sh.pipein(key)) 213 | with Shell() as sh: 214 | cert = sh.run('openssl', 'req', '-x509', '-days', '3650', '-sha256', '-subj', args.subject, '-key', sh.pipein(key)) 215 | with Shell() as sh: 216 | sh.run('yubico-piv-tool', '-s9c', '-aimport-certificate', '-i', sh.pipein(cert)) 217 | 218 | if args.action == 'init-ca': 219 | if not os.path.exists('.git'): 220 | subprocess.check_call(['git', 'init']) 221 | if not os.path.exists('certs'): 222 | os.mkdir('certs') 223 | for f in ('index.txt', 'index.txt.attr', 'ssh-index.txt', 'certs/.keep'): 224 | if not os.path.exists(f): 225 | with open(f, 'w+') as fd: 226 | pass 227 | for f in ('crlnumber', 'serial'): 228 | if not os.path.exists(f): 229 | with open(f, 'w+') as fd: 230 | fd.write('00\n') 231 | for f in ('index.txt', 'index.txt.attr', 'ssh-index.txt', 'certs', 'serial', 'crlnumber'): 232 | subprocess.check_call(['git', 'add', f]) 233 | subprocess.check_output(['git', 'commit', '-m', 'CA initialization']) 234 | 235 | if args.action == 'ssh-ca': 236 | sys.stdout.buffer.write(get_ssh_ca()) 237 | 238 | if args.action == 'ca-cert': 239 | sys.stdout.buffer.write(get_ca_cert()) 240 | 241 | if args.action == 'ssh-cert': 242 | keyfile = args.keyfile 243 | if not keyfile.endswith('.pub') and os.path.exists(keyfile + '.pub'): 244 | keyfile += '.pub' 245 | 246 | with open(keyfile) as fd: 247 | pubkey = fd.read().strip() 248 | 249 | sshca = get_ssh_ca() 250 | if args.id: 251 | keyid = args.id 252 | else: 253 | keyid = pubkey.split(maxsplit=2)[2] 254 | serial = int(os.urandom(8).hex(), 16) 255 | with Shell() as sh: 256 | ssh_keygen_args = ['-s', sh.pipein(sshca), '-D', pkcs11_module, '-I', keyid, '-z', str(serial)] 257 | if args.principals: 258 | ssh_keygen_args.append('-n') 259 | ssh_keygen_args.append(args.principals) 260 | if args.options: 261 | for opt in args.options: 262 | ssh_keygen_args.append('-O') 263 | ssh_keygen_args.append(opt) 264 | ssh_keygen_args.append(keyfile) 265 | sys.stdout.buffer.write(sh.run('ssh-keygen', *ssh_keygen_args)) 266 | pubkey_encoded = base64.b64encode(pubkey.encode()).decode() 267 | principals = (args.principals and args.principals) or '' 268 | now = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ') 269 | with open('ssh-index.txt', 'a+') as fd: 270 | fd.write('V serial:{serial} added:{now} pub:{pubkey_encoded} principals:{principals} id:{keyid}\n'.format(**locals())) 271 | ca_commit('SSH: Issuing certificate %s\n\nId: %s' % (serial, keyid)) 272 | 273 | if args.action == 'client-cert': 274 | key = open(args.keyfile, 'rb').read() 275 | with Shell() as sh: 276 | csr = sh.run('openssl', 'req', '-new', '-key', sh.pipein(key), '-sha256', '-days', '365', '-subj', args.subject) 277 | 278 | with open('serial') as fd: 279 | serial = fd.read() 280 | 281 | with Shell() as sh: 282 | cert = sh.run(*openssl_ca(sh), '-in', sh.pipein(csr), '-days', '365', '-notext') 283 | 284 | if args.pkcs12: 285 | with Shell() as sh: 286 | sh.run('openssl', 'pkcs12', '-export', '-in', sh.pipein(cert), '-inkey', sh.pipein(key), stdout=None) 287 | else: 288 | sys.stdout.buffer.write(cert) 289 | 290 | ca_commit('SSL: Issuing certificate %s\n\nSubject: %s' % (serial, args.subject)) 291 | 292 | if args.action == 'client-revoke': 293 | for serial in args.serial: 294 | with Shell() as sh: 295 | sh.run(*openssl_ca(sh), '-revoke', 'certs/%s.pem' % serial, stdout=None) 296 | ca_commit('SSL: Revokating certificates\n\nSerials: %s' % ', '.join(args.serial)) 297 | 298 | if args.action == 'crl': 299 | with Shell() as sh: 300 | sh.run(*openssl_ca(sh), '-gencrl', '-crldays', '3650', stdout=None) 301 | ca_commit('Issuing a new CRL') 302 | 303 | if args.action == 'ssh-revoke': 304 | serials = set("serial:%s" % s for s in args.serial) 305 | new_index = [] 306 | with open('ssh-index.txt') as fd: 307 | for entry in fd: 308 | validity, serial, data = entry.strip().split(maxsplit=2) 309 | if serial in serials: 310 | validity = 'R' 311 | new_index.append("%s %s %s" % (validity, serial, data)) 312 | with open('ssh-index.txt', 'w+') as fd: 313 | fd.write('\n'.join(new_index) + '\n') 314 | ca_commit('SSH: Revokating certificates\n\nSerials:\n%s\n' % '\n'.join(serials)) 315 | 316 | if args.action == 'krl': 317 | krl_data = [] 318 | with open('ssh-index.txt') as fd: 319 | for entry in fd: 320 | validity, serial, data = entry.strip().split(maxsplit=2) 321 | if validity == 'R': 322 | krl_data.append('serial: %s' % serial.split(':')[1]) 323 | with Shell() as sh: 324 | sh.run('ssh-keygen', '-k', '-f', sh.pipeout('krl'), '-s', sh.pipein(get_ssh_ca()), sh.pipein('\n'.join(krl_data).encode()), stdout=sys.stderr) 325 | sys.stdout.buffer.write(sh.out['krl']) 326 | --------------------------------------------------------------------------------