Commit cc9c2a1f authored by Peter Palfrader's avatar Peter Palfrader
Browse files

copy manage-backup-keys script

parent b64d5650
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+1 −0
Original line number Diff line number Diff line
backup-keys

README

0 → 100644
+14 −0
Original line number Diff line number Diff line
= Contingency Keys for HTTP Public Key Pinning =

We create a second set of keys for all our services, so we can pin keys to both
the one automatically generated on our letsencrypt host and the backup key.

If you do not have a backup-keys directory, get it from git:
 git clone pauli.torproject.org:/srv/puppet.torproject.org/git/tor-backup-keys.git backup-keys

After adding a new name to ./domains, run ./bin/manage-backup-keys create
and ./bin/manage-backup-keys verify.  Find the passphrase in
tor-passwords/000-backup-keys.

-- 
weasel, Fri, 23 Sep 2016 16:25:13 +0200

bin/get-pin

0 → 120000
+1 −0
Original line number Diff line number Diff line
manage-backup-keys
 No newline at end of file

bin/manage-backup-keys

0 → 100755
+253 −0
Original line number Diff line number Diff line
#!/usr/bin/python3

# Copyright (c) 2016 Peter Palfrader <peter@palfrader.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


# Create private RSA keys for all domains listed in domains.  Have these keys
# encrypted symmetrically, but we compute a digest of each key's public key
# and keep information for use with HTTP Public Key Pinning around.

import argparse
import base64
import getpass
import hashlib
import hmac
import logging
import os
import os.path
import shutil
import subprocess
import sys
import tempfile

class KeyHelpers:
    @staticmethod
    def get_pin_from_key(fn, passphrase=None):
        try:
            if not passphrase is None:
                (r_fd,w_fd) = os.pipe()

                # This assumes the passphrase fits into the buffer of our pipes
                os.write(w_fd, passphrase)
                os.close(w_fd)

                args = ['openssl', 'rsa', '-passin', 'fd:%d'%(r_fd,), '-in', fn, '-outform', 'der', '-pubout']
                logging.debug("Running {}.".format(' '.join(args)))
                pubkey = subprocess.check_output(args, pass_fds=[r_fd])
                os.close(r_fd)
            else:
                args = ['openssl', 'rsa', '-in', fn, '-outform', 'der', '-pubout']
                logging.debug("Running {}.".format(' '.join(args)))
                pubkey = subprocess.check_output(args)
        except subprocess.CalledProcessError:
            logging.error("openssl rsa failed to output public key from {}.  Wrong passphrase?".format(fn))
            sys.exit(1)

        digest = hashlib.sha256(pubkey).digest()
        encoded = base64.b64encode(digest).decode('ascii')
        pin = 'pin-sha256="' + encoded + '"'

        return pin

class KeyManager:
    PASSPHRASE_TOKEN = b'Yes, you have the right passphrase.'
    TOKEN_LENGTH = 4

    def __init__(self, keydir):
        if not os.path.isdir(keydir):
            logging.error("Key directory '{}' is not a directory.  Maybe you have to create it?".format(keydir))
            sys.exit(1)

        self.keydir = keydir


    def get_name_key(self, cn):
        return os.path.join(self.keydir, cn + '.key')
    def get_name_pin(self, cn):
        return os.path.join(self.keydir, cn + '.pin')
    def get_name_passphrase_token(self):
        return os.path.join(self.keydir, '.pass-token')

    def acquire_passphrase(self):
        passphrase = getpass.getpass(prompt='Passphrase for RSA keys: ')
        self.passphrase = passphrase.encode()

        fn = self.get_name_passphrase_token()

        token = hmac.new(self.passphrase, self.PASSPHRASE_TOKEN, hashlib.sha256).hexdigest()[0:self.TOKEN_LENGTH]

        if not os.path.exists(fn):
            logging.warning("Passphrase check token {} does not exist.  Creating it.".format(fn))
            with open(fn, "w") as f:
                print(token, file=f)

        with open(fn, "r") as f:
            token_from_file = f.read().strip()
        if not token_from_file == token:
            logging.warning("Passphrase check token {} mismatch!".format(fn))
            sys.exit(1)

class KeyCreator(KeyManager):

    @staticmethod
    def set_umask():
        os.umask(0o077)

    def create_key(self, tf):
        (r_fd,w_fd) = os.pipe()

        # This assumes the passphrase fits into the buffer of our pipes
        os.write(w_fd, self.passphrase)
        os.close(w_fd)

        subprocess.check_call(
            ['openssl', 'genrsa', '-aes256', '-passout', 'fd:%d'%(r_fd,), '-out', tf.name, '4096'],
            preexec_fn=self.set_umask,
            pass_fds=[r_fd])
        os.close(r_fd)

    def act(self, cn):
        keyname = self.get_name_key(cn)
        if os.path.exists(keyname):
            logging.info("Key for {} already exists.".format(cn))
            return True

        logging.info("Creating a key for {}.".format(cn))
        with tempfile.NamedTemporaryFile() as tf:
            self.create_key(tf)
            pin = KeyHelpers.get_pin_from_key(tf.name, passphrase=self.passphrase)
            with open(self.get_name_pin(cn), "w") as out:
                print(pin, file=out)
            shutil.copy(tf.name, keyname)

        return True


class KeyVerifier(KeyManager):
    def act(self, cn):
        keyname = self.get_name_key(cn)
        if not os.path.exists(keyname):
            logging.warning("Key for {} does not exist.".format(cn))
            return False

        pinfname = self.get_name_pin(cn)
        if not os.path.exists(pinfname):
            logging.warning("Digest file for {} does not exist.".format(cn))
            return False


        logging.debug("{}: Getting pin from file.".format(cn))
        with open(pinfname, "r") as f:
            pin_from_file = f.read().strip()
        logging.debug("{}:  It's '{}'.".format(cn, pin_from_file))

        logging.debug("{}: Getting pin from key.".format(cn))
        pin_from_key = KeyHelpers.get_pin_from_key(keyname, passphrase=self.passphrase)
        logging.debug("{}:  It's '{}'.".format(cn, pin_from_key))


        if not pin_from_file == pin_from_key:
            logging.warning("Pins for {} differ!".format(cn))
            return False

        logging.info("Pins for {} ok.".format(cn))
        return True


def get_domains(domains):
    for line in domains:
        line = line.strip()
        if line.startswith('#'):
            continue
        elems = line.split(maxsplit=1)
        if len(elems) == 0:
            continue
        yield elems[0]

def setup_logging(args):
    logargs = {'format': '%(levelname)s:%(message)s'}
    if args.verbose >= 2:
        logargs['level'] = logging.DEBUG
    elif args.verbose >= 1:
        logargs['level'] = logging.INFO
    else:
        logargs['level'] = logging.WARNING
    logging.basicConfig(**logargs)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("command",
        help="One of 'create', 'verify'.")
    parser.add_argument("--domains",
        type=argparse.FileType('r'),
        default='domains',
        help="file with list of domains")
    parser.add_argument("--keydir",
        default='backup-keys',
        help="directory for keys")
    parser.add_argument('--verbose', '-v',
        action='count',
        default=0,
        help="more verbose output (use twice for even more)")
    args = parser.parse_args()

    setup_logging(args)

    if args.command == 'create':
        cmd = KeyCreator(args.keydir)
    elif args.command == 'verify':
        cmd = KeyVerifier(args.keydir)
    else:
        logging.error("Unknown command '{}'.".format(args.command))
        sys.exit(1)

    cmd.acquire_passphrase()

    ok = True
    for d in get_domains(args.domains):
        res = cmd.act(d)
        if not res: ok = False

    if not ok:
        sys.exit(1)

def get_pin():
    parser = argparse.ArgumentParser()
    parser.add_argument("keyfile",
        help="File with (unprotected) key")
    parser.add_argument('--verbose', '-v',
        action='count',
        default=0,
        help="more verbose output (use twice for even more)")
    args = parser.parse_args()

    setup_logging(args)

    pin = KeyHelpers.get_pin_from_key(args.keyfile)
    print(pin)

if __name__ == "__main__":
    cmd_name =  os.path.basename(sys.argv[0])
    if cmd_name == "get-pin":
        get_pin()
    else:
        main()