diff --git a/.gitignore b/.gitignore index 9777179..640646b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ htmlcov/ libs/ publish/ venv/ +aws_lambda_libs/ +lambda_configs/ +.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 11edb2c..3464219 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ -sudo: false - language: python addons: matrix: include: - - python: "2.7" + - python: "3.7" install: - pip install coveralls diff --git a/Makefile b/Makefile index 1fabaf2..a50356d 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ test: lint develop: @echo "--> Installing dependencies" + pip install --upgrade pip setuptools pip install -r requirements.txt pip install "file://`pwd`#egg=bless[tests]" @echo "" @@ -33,21 +34,16 @@ publish: rm -rf ./publish/bless_lambda/ mkdir -p ./publish/bless_lambda cp -r ./bless ./publish/bless_lambda/ - mv ./publish/bless_lambda/bless/aws_lambda/* ./publish/bless_lambda/ + cp ./publish/bless_lambda/bless/aws_lambda/bless* ./publish/bless_lambda/ cp -r ./aws_lambda_libs/. ./publish/bless_lambda/ - cp -r ./lambda_configs/. ./publish/bless_lambda/ - cd ./publish/bless_lambda && zip -r ../bless_lambda.zip . + if [ -d ./lambda_configs/ ]; then cp -r ./lambda_configs/. ./publish/bless_lambda/; fi + cd ./publish/bless_lambda && zip -FSr ../bless_lambda.zip . compile: - yum install -y gcc libffi-devel openssl-devel python27-virtualenv - virtualenv /tmp/venv - /tmp/venv/bin/pip install --upgrade pip setuptools - /tmp/venv/bin/pip install -e . - cp -r /tmp/venv/lib/python2.7/site-packages/. ./aws_lambda_libs - cp -r /tmp/venv/lib64/python2.7/site-packages/. ./aws_lambda_libs + ./lambda_compile.sh lambda-deps: @echo "--> Compiling lambda dependencies" - docker run --rm -it -v ${CURDIR}:/src -w /src amazonlinux make compile + docker run --rm -v ${CURDIR}:/src -w /src amazonlinux:2 ./lambda_compile.sh .PHONY: develop dev-docs clean test lint coverage publish diff --git a/README.md b/README.md index 2967b12..875c698 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ # BLESS - Bastion's Lambda Ephemeral SSH Service [![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Test coverage](https://coveralls.io/repos/github/Netflix/bless/badge.svg?branch=master)](https://coveralls.io/github/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/bless.svg)]() -BLESS is an SSH Certificate Authority that runs as a AWS Lambda function and is used to sign ssh +BLESS is an SSH Certificate Authority that runs as an AWS Lambda function and is used to sign SSH public keys. -SSH Certificates are an excellent way to authorize users to access a particular ssh host, +SSH Certificates are an excellent way to authorize users to access a particular SSH host, as they can be restricted for a single use case, and can be short lived. Instead of managing the authorized_keys of a host, or controlling who has access to SSH Private Keys, hosts just need to be configured to trust an SSH CA. @@ -33,7 +33,7 @@ Cd to the bless repo: Create a virtualenv if you haven't already: - $ virtualenv venv + $ python3.7 -m venv venv Activate the venv: @@ -52,42 +52,29 @@ Run the tests: To deploy an AWS Lambda Function, you need to provide a .zip with the code and all dependencies. The .zip must contain your lambda code and configurations at the top level of the .zip. The BLESS Makefile includes a publish target to package up everything into a deploy-able .zip if they are in -the expected locations. +the expected locations. You will need to setup your own Python 3.7 lambda to deploy the .zip to. + +Previously the AWS Lambda Handler needed to be set to `bless_lambda.lambda_handler`, and this would generate a user +cert. `bless_lambda.lambda_handler` still works for user certs. `bless_lambda_user.lambda_handler_user` is a handler +that can also be used to issue user certificates. + +A new handler `bless_lambda_host.lambda_handler_host` has been created to allow for the creation of host SSH certs. + +All three handlers exist in the published .zip. ### Compiling BLESS Lambda Dependencies -AWS Lambda has some limitations, and to deploy code as a Lambda Function, you need to package up -all of the dependencies. AWS Lambda only supports Python 2.7 and BLESS depends on -[Cryptography](https://cryptography.io/en/latest/), which must be compiled. You will need to +To deploy code as a Lambda Function, you need to package up all of the dependencies. You will need to compile and include your dependencies before you can publish a working AWS Lambda. -You can use a docker container running amazon linux: +BLESS uses a docker container running [Amazon Linux 2](https://hub.docker.com/_/amazonlinux) to package everything up: - Execute ```make lambda-deps``` and this will run a container and save all the dependencies in ./aws_lambda_libs -Alternatively you can: -- Deploy an [Amazon Linux AMI](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) -- SSH onto that instance -- Copy BLESS' `setup.py` to the instance -- Copy BLESS' `bless/__about__.py` to the instance at `bless/__about__.py` -- Install BLESS' dependencies: -``` -$ sudo yum install gcc libffi-devel openssl-devel -$ virtualenv venv -$ source venv/bin/activate -(venv) $ pip install --upgrade pip setuptools -(venv) $ pip install -e . -``` -- From that instance, copy off the contents of: -``` -$ cp -r venv/lib/python2.7/site-packages/. aws_lambda_libs -$ cp -r venv/lib64/python2.7/site-packages/. aws_lambda_libs -``` -- put those files in: ./aws_lambda_libs/ - ### Protecting the CA Private Key -- Generate a password protected RSA Private Key: +- Generate a password protected RSA Private Key in the PEM format: ``` -$ ssh-keygen -t rsa -b 4096 -f bless-ca- -C "SSH CA Key" +$ ssh-keygen -t rsa -b 4096 -m PEM -f bless-ca- -C "SSH CA Key" ``` +- **Note:** OpenSSH Private Key format is not supported. - Use KMS to encrypt your password. You will need a KMS key per region, and you will need to encrypt your password for each region. You can use the AWS Console to paste in a simple lambda function like this: @@ -114,13 +101,23 @@ def lambda_handler(event, context): - Provide your desired ./lambda_configs/ca_key_name.pem prior to Publishing a new Lambda .zip - Set the permissions of ./lambda_configs/ca_key_name.pem to 444. +You can now provide your private key and/or encrypted private key password via the lambda environment or config file. +In the `[Bless CA]` section, you can set `ca_private_key` instead of the `ca_private_key_file` with a base64 encoded +version of your .pem (e.g. `cat key.pem | base64` ). + +Because every config file option is supported in the environment, you can also just set `bless_ca_default_password` +and/or `bless_ca_ca_private_key`. Due to limits on AWS Lambda environment variables, you'll need to compress RSA 4096 +private keys, which you can now do by setting `bless_ca_ca_private_key_compression`. For example, set +`bless_ca_ca_private_key_compression = bz2` and `bless_ca_ca_private_key` to the output of +`cat ca-key.pem | bzip2 | base64`. + ### BLESS Config File - Refer to the the [Example BLESS Config File](bless/config/bless_deploy_example.cfg) and its included documentation. - Manage your bless_deploy.cfg files outside of this repo. - Provide your desired ./lambda_configs/bless_deploy.cfg prior to Publishing a new Lambda .zip - The required [Bless CA] option values must be set for your environment. -- Every option can be changed in the environment. The environment variable name is contructed +- Every option can be changed in the environment. The environment variable name is constructed as section_name_option_name (all lowercase, spaces replaced with underscores). ### Publish Lambda .zip @@ -152,6 +149,8 @@ random from kms (kms:GenerateRandom) and permissions for logging to CloudWatch L ## Using BLESS After you have [deployed BLESS](#deployment) you can run the sample [BLESS Client](bless_client/bless_client.py) from a system with access to the required [AWS Credentials](http://boto3.readthedocs.io/en/latest/guide/configuration.html). +This client is really just a proof of concept to validate that you have a functional lambda being called with valid +IAM credentials. (venv) $ ./bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_usernames bastion_source_ip bastion_command @@ -162,11 +161,11 @@ You can inspect the contents of a certificate with ssh-keygen directly: $ ssh-keygen -L -f your-cert.pub ## Enabling BLESS Certificates On Servers -Add the following line to /etc/ssh/sshd_config: +Add the following line to `/etc/ssh/sshd_config`: TrustedUserCAKeys /etc/ssh/cas.pub -Add a new file, owned by and only writable by root, at /etc/ssh/cas.pub with the contents: +Add a new file, owned by and only writable by root, at `/etc/ssh/cas.pub` with the contents: ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… #id_rsa.pub of an SSH CA ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… #id_rsa.pub of an offline SSH CA diff --git a/bless/__about__.py b/bless/__about__.py index 89972af..6d53e01 100644 --- a/bless/__about__.py +++ b/bless/__about__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - __all__ = [ "__title__", "__summary__", "__uri__", "__version__", "__author__", "__email__", "__license__", "__copyright__", @@ -11,7 +9,7 @@ "sign SSH public keys.") __uri__ = "https://github.com/Netflix/bless" -__version__ = "0.2.0" +__version__ = "0.4.0" __author__ = "The BLESS developers" __email__ = "security@netflix.com" diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 8a482ab..f04e894 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,191 +3,11 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -import base64 -import logging -import os -import time +from bless.aws_lambda.bless_lambda_user import lambda_handler_user -import boto3 -from botocore.exceptions import ClientError -from kmsauth import KMSTokenValidator, TokenValidationError -from marshmallow.exceptions import ValidationError -from bless.config.bless_config import BlessConfig, \ - BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ - CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ - ENTROPY_MINIMUM_BITS_OPTION, \ - RANDOM_SEED_BYTES_OPTION, \ - LOGGING_LEVEL_OPTION, \ - KMSAUTH_SECTION, \ - KMSAUTH_USEKMSAUTH_OPTION, \ - KMSAUTH_SERVICE_ID_OPTION, \ - TEST_USER_OPTION, \ - CERTIFICATE_EXTENSIONS_OPTION -from bless.request.bless_request import BlessSchema -from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ - get_ssh_certificate_authority -from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType -from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder - - -def lambda_handler(event, context=None, ca_private_key_password=None, - entropy_check=True, - config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')): +def lambda_handler(*args, **kwargs): """ - This is the function that will be called when the lambda function starts. - :param event: Dictionary of the json request. - :param context: AWS LambdaContext Object - http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html - :param ca_private_key_password: For local testing, if the password is provided, skip the KMS - decrypt. - :param entropy_check: For local testing, if set to false, it will skip checking entropy and - won't try to fetch additional random from KMS - :param config_file: The config file to load the SSH CA private key from, and additional settings - :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + Wrapper around lambda_handler_user for backwards compatibility """ - # AWS Region determines configs related to KMS - region = os.environ['AWS_REGION'] - - # Load the deployment config values - config = BlessConfig(region, - config_file=config_file) - - logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) - numeric_level = getattr(logging, logging_level.upper(), None) - if not isinstance(numeric_level, int): - raise ValueError('Invalid log level: {}'.format(logging_level)) - - logger = logging.getLogger() - logger.setLevel(numeric_level) - - certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) - certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) - entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION) - random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION) - ca_private_key = config.getprivatekey() - password_ciphertext_b64 = config.getpassword() - certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) - - # Process cert request - schema = BlessSchema(strict=True) - try: - request = schema.load(event).data - except ValidationError as e: - return error_response('InputValidationError', str(e)) - - logger.info('Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'.format( - request.bastion_user, - request.bastion_user_ip, - request.public_key_to_sign, - request.kmsauth_token)) - - # decrypt ca private key password - if ca_private_key_password is None: - kms_client = boto3.client('kms', region_name=region) - try: - ca_password = kms_client.decrypt( - CiphertextBlob=base64.b64decode(password_ciphertext_b64)) - ca_private_key_password = ca_password['Plaintext'] - except ClientError as e: - return error_response('ClientError', str(e)) - - # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired - if entropy_check: - with open('/proc/sys/kernel/random/entropy_avail', 'r') as f: - entropy = int(f.read()) - logger.debug(entropy) - if entropy < entropy_minimum_bits: - logger.info( - 'System entropy was {}, which is lower than the entropy_' - 'minimum {}. Using KMS to seed /dev/urandom'.format( - entropy, entropy_minimum_bits)) - response = kms_client.generate_random( - NumberOfBytes=random_seed_bytes) - random_seed = response['Plaintext'] - with open('/dev/urandom', 'w') as urandom: - urandom.write(random_seed) - - # cert values determined only by lambda and its configs - current_time = int(time.time()) - test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION) - if (test_user and (request.bastion_user == test_user or - request.remote_usernames == test_user)): - # This is a test call, the lambda will issue an invalid - # certificate where valid_before < valid_after - valid_before = current_time - valid_after = current_time + 1 - bypass_time_validity_check = True - else: - valid_before = current_time + certificate_validity_after_seconds - valid_after = current_time - certificate_validity_before_seconds - bypass_time_validity_check = False - - # Authenticate the user with KMS, if key is setup - if config.get(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION): - if request.kmsauth_token: - try: - validator = KMSTokenValidator( - None, - config.getkmsauthkeyids(), - config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION), - region - ) - # decrypt_token will raise a TokenValidationError if token doesn't match - validator.decrypt_token( - "2/user/{}".format(request.remote_usernames), - request.kmsauth_token - ) - except TokenValidationError as e: - return error_response('KMSAuthValidationError', str(e)) - else: - return error_response('InputValidationError', 'Invalid request, missing kmsauth token') - - # Build the cert - ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) - cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, - request.public_key_to_sign) - for username in request.remote_usernames.split(','): - cert_builder.add_valid_principal(username) - - cert_builder.set_valid_before(valid_before) - cert_builder.set_valid_after(valid_after) - - if certificate_extensions: - for e in certificate_extensions.split(','): - if e: - cert_builder.add_extension(e) - else: - cert_builder.clear_extensions() - - # cert_builder is needed to obtain the SSH public key's fingerprint - key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( - context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command, - cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) - cert_builder.set_critical_option_source_addresses(request.bastion_ips) - cert_builder.set_key_id(key_id) - cert = cert_builder.get_cert_file(bypass_time_validity_check) - - logger.info( - 'Issued a cert to bastion_ips[{}] for remote_usernames[{}] with key_id[{}] and ' - 'valid_from[{}])'.format( - request.bastion_ips, request.remote_usernames, key_id, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) - return success_response(cert) - - -def success_response(cert): - return { - 'certificate': cert - } - - -def error_response(error_type, error_message): - return { - 'errorType': error_type, - 'errorMessage': error_message - } + return lambda_handler_user(*args, **kwargs) diff --git a/bless/aws_lambda/bless_lambda_common.py b/bless/aws_lambda/bless_lambda_common.py new file mode 100644 index 0000000..41ccbf0 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_common.py @@ -0,0 +1,75 @@ +""" +.. module: bless.aws_lambda.bless_lambda_common + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import logging +import os + +import boto3 +from bless.cache.bless_lambda_cache import BlessLambdaCache +from bless.config.bless_config import BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION, ENTROPY_MINIMUM_BITS_OPTION, \ + RANDOM_SEED_BYTES_OPTION + +global_bless_cache = None + + +def success_response(cert): + return { + 'certificate': cert + } + + +def error_response(error_type, error_message): + return { + 'errorType': error_type, + 'errorMessage': error_message + } + + +def set_logger(config): + logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) + numeric_level = getattr(logging, logging_level.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: {}'.format(logging_level)) + + logger = logging.getLogger() + logger.setLevel(numeric_level) + return logger + + +def check_entropy(config, logger): + """ + Check the entropy pool and seed it with KMS if desired + """ + region = os.environ['AWS_REGION'] + kms_client = boto3.client('kms', region_name=region) + entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION) + random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION) + + with open('/proc/sys/kernel/random/entropy_avail', 'r') as f: + entropy = int(f.read()) + logger.debug(entropy) + if entropy < entropy_minimum_bits: + logger.info( + 'System entropy was {}, which is lower than the entropy_' + 'minimum {}. Using KMS to seed /dev/urandom'.format( + entropy, entropy_minimum_bits)) + response = kms_client.generate_random( + NumberOfBytes=random_seed_bytes) + random_seed = response['Plaintext'] + with open('/dev/urandom', 'w') as urandom: + urandom.write(random_seed) + + +def setup_lambda_cache(ca_private_key_password, config_file): + # For testing, ignore the static bless_cache, otherwise fill the cache one time. + global global_bless_cache + if ca_private_key_password is not None or config_file is not None: + bless_cache = BlessLambdaCache(ca_private_key_password, config_file) + elif global_bless_cache is None: + global_bless_cache = BlessLambdaCache(config_file=os.path.join(os.getcwd(), 'bless_deploy.cfg')) + bless_cache = global_bless_cache + else: + bless_cache = global_bless_cache + return bless_cache diff --git a/bless/aws_lambda/bless_lambda_host.py b/bless/aws_lambda/bless_lambda_host.py new file mode 100644 index 0000000..91ff1ab --- /dev/null +++ b/bless/aws_lambda/bless_lambda_host.py @@ -0,0 +1,102 @@ +""" +.. module: bless.aws_lambda.bless_lambda_host + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import time + +from bless.aws_lambda.bless_lambda_common import success_response, error_response, set_logger, check_entropy, \ + setup_lambda_cache +from bless.config.bless_config import BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, HOSTNAME_VALIDATION_OPTION +from bless.request.bless_request_host import BlessHostSchema +from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import get_ssh_certificate_authority +from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType +from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder +from marshmallow import ValidationError + + +def lambda_handler_host( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) + + # Load the deployment config values + config = bless_cache.config + + logger = set_logger(config) + + certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + + ca_private_key = config.getprivatekey() + + # Process cert request + schema = BlessHostSchema(strict=True) + schema.context[HOSTNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION) + + try: + request = schema.load(event).data + except ValidationError as e: + return error_response('InputValidationError', str(e)) + + # todo: You'll want to bring your own hostnames validation. + logger.info('Bless lambda invoked by [public_key: {}] for hostnames[{}]'.format(request.public_key_to_sign, + request.hostnames)) + + # Make sure we have the ca private key password + if bless_cache.ca_private_key_password is None: + return error_response('ClientError', bless_cache.ca_private_key_password_error) + else: + ca_private_key_password = bless_cache.ca_private_key_password + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + check_entropy(config, logger) + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + valid_before = current_time + certificate_validity_after_seconds + valid_after = current_time - certificate_validity_before_seconds + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.HOST, + request.public_key_to_sign) + + for hostname in request.hostnames.split(','): + cert_builder.add_valid_principal(hostname) + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( + context.aws_request_id, cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)) + ) + + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + logger.info( + 'Issued a server cert to hostnames[{}] with key_id[{}] and ' + 'valid_from[{}])'.format( + request.hostnames, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return success_response(cert) diff --git a/bless/aws_lambda/bless_lambda_user.py b/bless/aws_lambda/bless_lambda_user.py new file mode 100644 index 0000000..166bec9 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_user.py @@ -0,0 +1,193 @@ +""" +.. module: bless.aws_lambda.bless_lambda_user + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import time + +import boto3 +from bless.aws_lambda.bless_lambda_common import success_response, error_response, set_logger, check_entropy, \ + setup_lambda_cache +from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + USERNAME_VALIDATION_OPTION, \ + KMSAUTH_SECTION, \ + KMSAUTH_USEKMSAUTH_OPTION, \ + KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION, \ + VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION, \ + KMSAUTH_SERVICE_ID_OPTION, \ + TEST_USER_OPTION, \ + CERTIFICATE_EXTENSIONS_OPTION, \ + REMOTE_USERNAMES_VALIDATION_OPTION, \ + IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION, \ + REMOTE_USERNAMES_BLACKLIST_OPTION +from bless.request.bless_request_user import BlessUserSchema +from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ + get_ssh_certificate_authority +from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType +from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder +from kmsauth import KMSTokenValidator, TokenValidationError +from marshmallow.exceptions import ValidationError + + +def lambda_handler_user( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) + + # AWS Region determines configs related to KMS + region = bless_cache.region + + # Load the deployment config values + config = bless_cache.config + + logger = set_logger(config) + + certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + ca_private_key = config.getprivatekey() + certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) + + # Process cert request + schema = BlessUserSchema(strict=True) + schema.context[USERNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) + schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, + REMOTE_USERNAMES_VALIDATION_OPTION) + schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = config.get(BLESS_OPTIONS_SECTION, + REMOTE_USERNAMES_BLACKLIST_OPTION) + + try: + request = schema.load(event).data + except ValidationError as e: + return error_response('InputValidationError', str(e)) + + logger.info('Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'.format( + request.bastion_user, + request.bastion_user_ip, + request.public_key_to_sign, + request.kmsauth_token)) + + # Make sure we have the ca private key password + if bless_cache.ca_private_key_password is None: + return error_response('ClientError', bless_cache.ca_private_key_password_error) + else: + ca_private_key_password = bless_cache.ca_private_key_password + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + check_entropy(config, logger) + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION) + if test_user and (request.bastion_user == test_user or request.remote_usernames == test_user): + # This is a test call, the lambda will issue an invalid + # certificate where valid_before < valid_after + valid_before = current_time + valid_after = current_time + 1 + bypass_time_validity_check = True + else: + valid_before = current_time + certificate_validity_after_seconds + valid_after = current_time - certificate_validity_before_seconds + bypass_time_validity_check = False + + # Authenticate the user with KMS, if key is setup + if config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION): + if request.kmsauth_token: + # Allow bless to sign the cert for a different remote user than the name of the user who signed it + allowed_remotes = config.get(KMSAUTH_SECTION, KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION) + if allowed_remotes: + allowed_users = allowed_remotes.split(',') + requested_remotes = request.remote_usernames.split(',') + if allowed_users != ['*'] and not all([u in allowed_users for u in requested_remotes]): + return error_response('KMSAuthValidationError', + 'unallowed remote_usernames [{}]'.format(request.remote_usernames)) + + # Check if the user is in the required IAM groups + if config.getboolean(KMSAUTH_SECTION, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION): + iam = boto3.client('iam') + user_groups = iam.list_groups_for_user(UserName=request.bastion_user) + + group_name_template = config.get(KMSAUTH_SECTION, IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION) + for requested_remote in requested_remotes: + required_group_name = group_name_template.format(requested_remote) + + user_is_in_group = any( + group + for group in user_groups['Groups'] + if group['GroupName'] == required_group_name + ) + + if not user_is_in_group: + return error_response('KMSAuthValidationError', + 'user {} is not in the {} iam group'.format(request.bastion_user, + required_group_name)) + + elif request.remote_usernames != request.bastion_user: + return error_response('KMSAuthValidationError', + 'remote_usernames must be the same as bastion_user') + try: + validator = KMSTokenValidator( + None, + config.getkmsauthkeyids(), + config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION), + region + ) + # decrypt_token will raise a TokenValidationError if token doesn't match + validator.decrypt_token( + "2/user/{}".format(request.bastion_user), + request.kmsauth_token + ) + except TokenValidationError as e: + return error_response('KMSAuthValidationError', str(e)) + else: + return error_response('InputValidationError', 'Invalid request, missing kmsauth token') + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, + request.public_key_to_sign) + for username in request.remote_usernames.split(','): + cert_builder.add_valid_principal(username) + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + if certificate_extensions: + for e in certificate_extensions.split(','): + if e: + cert_builder.add_extension(e) + else: + cert_builder.clear_extensions() + + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( + context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command, + cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) + cert_builder.set_critical_option_source_addresses(request.bastion_ips) + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file(bypass_time_validity_check) + + logger.info( + 'Issued a cert to bastion_ips[{}] for remote_usernames[{}] with key_id[{}] and ' + 'valid_from[{}])'.format( + request.bastion_ips, request.remote_usernames, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return success_response(cert) diff --git a/bless/cache/__init__.py b/bless/cache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/cache/bless_lambda_cache.py b/bless/cache/bless_lambda_cache.py new file mode 100644 index 0000000..afc4f1c --- /dev/null +++ b/bless/cache/bless_lambda_cache.py @@ -0,0 +1,49 @@ +""" +.. module: bless.cache.bless_lambda_cache + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import os + +import boto3 +from bless.config.bless_config import BlessConfig +from botocore.exceptions import ClientError + + +class BlessLambdaCache: + region = None + config = None + ca_private_key_password = None + ca_private_key_password_error = None + + def __init__(self, ca_private_key_password=None, + config_file=None): + """ + + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + """ + # AWS Region determines configs related to KMS + if 'AWS_REGION' in os.environ: + self.region = os.environ['AWS_REGION'] + else: + self.region = 'us-west-2' + + # Load the deployment config values + self.config = BlessConfig(self.region, config_file=config_file) + + password_ciphertext_b64 = self.config.getpassword() + + # decrypt ca private key password + if ca_private_key_password is None: + kms_client = boto3.client('kms', region_name=self.region) + try: + ca_password = kms_client.decrypt( + CiphertextBlob=base64.b64decode(password_ciphertext_b64)) + self.ca_private_key_password = ca_password['Plaintext'] + except ClientError as e: + self.ca_private_key_password_error = str(e) + else: + self.ca_private_key_password = ca_private_key_password diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 95fbce7..e5cf4a9 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -3,15 +3,21 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -import ConfigParser +import configparser import base64 import os import re +import zlib +import bz2 BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds' CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 +SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'server_certificate_validity_before_seconds' +SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT = 120 +SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'server_certificate_validity_after_seconds' +SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT = 31536000 ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' ENTROPY_MINIMUM_BITS_DEFAULT = 2048 @@ -33,24 +39,47 @@ 'permit-pty,' \ 'permit-user-rc' +HOSTNAME_VALIDATION_OPTION = 'hostname_validation' +HOSTNAME_VALIDATION_DEFAULT = 'url' + BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' CA_PRIVATE_KEY_OPTION = 'ca_private_key' +CA_PRIVATE_KEY_COMPRESSION_OPTION = 'ca_private_key_compression' +CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT = None REGION_PASSWORD_OPTION_SUFFIX = '_password' KMSAUTH_SECTION = 'KMS Auth' KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth' -KMSAUTH_USEKMSAUTH_DEFAULT = False +KMSAUTH_USEKMSAUTH_DEFAULT = "False" KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' KMSAUTH_KEY_ID_DEFAULT = '' +KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION = 'kmsauth_remote_usernames_allowed' +KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT = None + KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid' KMSAUTH_SERVICE_ID_DEFAULT = None +USERNAME_VALIDATION_OPTION = 'username_validation' +USERNAME_VALIDATION_DEFAULT = 'useradd' + +REMOTE_USERNAMES_VALIDATION_OPTION = 'remote_usernames_validation' +REMOTE_USERNAMES_VALIDATION_DEFAULT = 'principal' + +VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION = 'kmsauth_validate_remote_usernames_against_iam_groups' +VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT = "False" + +IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION = 'kmsauth_iam_group_name_format' +IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT = 'ssh-{}' -class BlessConfig(ConfigParser.RawConfigParser, object): +REMOTE_USERNAMES_BLACKLIST_OPTION = 'remote_usernames_blacklist' +REMOTE_USERNAMES_BLACKLIST_DEFAULT = None + + +class BlessConfig(configparser.RawConfigParser, object): def __init__(self, aws_region, config_file): """ Parses the BLESS config file, and provides some reasonable default values if they are @@ -71,12 +100,25 @@ def __init__(self, aws_region, config_file): TEST_USER_OPTION: TEST_USER_DEFAULT, KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT, KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, + KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION: KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT, KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, - CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT + CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, + USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, + REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT, + VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION: VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT, + IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION: IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT, + REMOTE_USERNAMES_BLACKLIST_OPTION: REMOTE_USERNAMES_BLACKLIST_DEFAULT, + CA_PRIVATE_KEY_COMPRESSION_OPTION: CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION: SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION: SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, + HOSTNAME_VALIDATION_OPTION: HOSTNAME_VALIDATION_DEFAULT } - ConfigParser.RawConfigParser.__init__(self, defaults=defaults) + configparser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) + if not self.has_section(BLESS_CA_SECTION): + self.add_section(BLESS_CA_SECTION) + if not self.has_section(BLESS_OPTIONS_SECTION): self.add_section(BLESS_OPTIONS_SECTION) @@ -102,17 +144,24 @@ def getkmsauthkeyids(self): in one region can validate in another). :return: A list of kmsauth key ids """ - return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) + return list(map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(','))) def getprivatekey(self): + """ + Get a private key from either a file specified in the config file, or from an environment variable. Env + Vars in Lambda can't contain a 4096 RSA key uncompressed, so compressed keys are also supported. + :return: byte string that contains the private key in PEM format (ascii). + """ + compression = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION) + if self.has_option(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION): - return base64.b64decode(self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION)) + return self._decompress(base64.b64decode(self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION)), compression) ca_private_key_file = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) # read the private key .pem - with open(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, ca_private_key_file), 'r') as f: - return f.read() + with open(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, ca_private_key_file), 'rb') as f: + return self._decompress(f.read(), compression) def has_option(self, section, option): """ @@ -129,7 +178,7 @@ def has_option(self, section, option): else: return super(BlessConfig, self).has_option(section, option) - def get(self, section, option): + def get(self, section, option, **kwargs): """ Gets a value from the configuration. @@ -141,9 +190,28 @@ def get(self, section, option): environment_key = self._environment_key(section, option) output = os.environ.get(environment_key, None) if output is None: - output = super(BlessConfig, self).get(section, option) + output = super(BlessConfig, self).get(section, option, **kwargs) return output @staticmethod def _environment_key(section, option): - return (re.sub('\W+', '_', section) + '_' + re.sub('\W+', '_', option)).lower() + return (re.sub(r'\W+', '_', section) + '_' + re.sub(r'\W+', '_', option)).lower() + + @staticmethod + def _decompress(data, algorithm): + """ + Decompress a byte string based of the provided algorithm. + :param data: byte string + :param algorithm: string with the name of the compression algorithm used + :return: decompressed byte string. + """ + if algorithm is None or algorithm == 'none': + result = data + elif algorithm == 'zlib': + result = zlib.decompress(data) + elif algorithm == 'bz2': + result = bz2.decompress(data) + else: + raise ValueError("Compression {} is not supported.".format(algorithm)) + + return result diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index a01f65a..0bc94ef 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -11,6 +11,18 @@ random_seed_bytes = 256 logging_level = INFO # Comma separated list of the SSH Certificate extensions to include. Not specifying this uses the ssh-keygen defaults: # certificate_extensions = permit-X11-forwarding,permit-agent-forwarding,permit-port-forwarding,permit-pty,permit-user-rc +# Username validation options are described in bless_request_user.py:USERNAME_VALIDATION_OPTIONS +# Configure how bastion_user names are validated. +# username_validation = useradd +# Configure how remote_usernames names are validated. +# remote_usernames_validation = principal +# Configure a regex of blacklisted remote_usernames that will be rejected for any value of remote_usernames_validation. +# remote_usernames_blacklist = root|admin.* +# Number of seconds +/- the issued time for the server certificates to be valid +# server_certificate_validity_before_seconds = 120 +# server_certificate_validity_after_seconds = 31536000 +# Configure how server certificate hostnames are validated +# hostname_validation = url # These values are all required to be modified for deployment [Bless CA] @@ -36,3 +48,18 @@ ca_private_key_file = # If using kmsauth, you need to set the kmsauth service name. Users need to set the 'to' # context to this same service name when they create a kmsauth token. # kmsauth_serviceid = bless-production + +# By default, kmsauth requires that requested bastion_user must be the same as the requested remote_usernames. If you +# want Bless to sign a certificate for a different remote_usernames (like root, or a shared admin account), you must +# specify those allowed names here. * will allow signing for all remote_usernames +# kmsauth_remote_usernames_allowed = ubuntu,root,ec2-user,stufflikethat + +# If the kmsauth_remote_usernames_allowed option is set, kmsauth will allow certifiates for those usernames +# to be generated by any user who can invoke the lambda function. If you would like to ensure that users have to +# be in a an IAM group pertaining to the remote_username, enable this option. +# kmsauth_validate_remote_usernames_against_iam_groups = False + +# For use with the kmsauth_validate_remote_usernames_against_iam_groups option. By default the required format for +# the group name is "ssh-{}".format(remote_username), but that can be changed here. The groups must have a +# consistent naming scheme and must all contain the remote_username once. For example, ssh-ubuntu. +# kmsauth_iam_group_name_format = ssh-{} diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py deleted file mode 100644 index 16ccaaf..0000000 --- a/bless/request/bless_request.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -.. module: bless.request.bless_request - :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. -""" -import re - -import ipaddress -from marshmallow import Schema, fields, post_load, ValidationError, validates_schema - -# man 8 useradd -USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') -# It appears that most printable ascii is valid, excluding whitespace, #, and commas. -# There doesn't seem to be any practical size limits of a principal (> 4096B allowed). -PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') -VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" - - -def validate_ips(ips): - try: - for ip in ips.split(','): - ipaddress.ip_network(ip, strict=True) - except ValueError: - raise ValidationError('Invalid IP address.') - - -def validate_user(user): - if len(user) > 32: - raise ValidationError('Username is too long.') - if USERNAME_PATTERN.match(user) is None: - raise ValidationError('Username contains invalid characters.') - - -def validate_principals(principals): - for principal in principals.split(','): - if PRINCIPAL_PATTERN.match(principal) is None: - raise ValidationError('Principal contains invalid characters.') - - -def validate_ssh_public_key(public_key): - if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER): - pass - # todo other key types - else: - raise ValidationError('Invalid SSH Public Key.') - - -class BlessSchema(Schema): - bastion_ips = fields.Str(validate=validate_ips, required=True) - bastion_user = fields.Str(validate=validate_user, required=True) - bastion_user_ip = fields.Str(validate=validate_ips, required=True) - command = fields.Str(required=True) - public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) - remote_usernames = fields.Str(validate=validate_principals, required=True) - kmsauth_token = fields.Str(required=False) - - @validates_schema(pass_original=True) - def check_unknown_fields(self, data, original_data): - unknown = set(original_data) - set(self.fields) - if unknown: - raise ValidationError('Unknown field', unknown) - - @post_load - def make_bless_request(self, data): - return BlessRequest(**data) - - -class BlessRequest: - def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, - remote_usernames, kmsauth_token=None): - """ - A BlessRequest must have the following key value pairs to be valid. - :param bastion_ips: The source IPs where the SSH connection will be initiated from. This is - enforced in the issued certificate. - :param bastion_user: The user on the bastion, who is initiating the SSH request. - :param bastion_user_ip: The IP of the user accessing the bastion. - :param command: Text information about the SSH request of the user. - :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is - enforced in the issued certificate. - :param remote_usernames: Comma-separated list of username(s) or authorized principals on the remote - server that will be used in the SSH request. This is enforced in the issued certificate. - :param kmsauth_token: An optional kms auth token to authenticate the user. - """ - self.bastion_ips = bastion_ips - self.bastion_user = bastion_user - self.bastion_user_ip = bastion_user_ip - self.command = command - self.public_key_to_sign = public_key_to_sign - self.remote_usernames = remote_usernames - self.kmsauth_token = kmsauth_token - - def __eq__(self, other): - return self.__dict__ == other.__dict__ diff --git a/bless/request/bless_request_common.py b/bless/request/bless_request_common.py new file mode 100644 index 0000000..97f9451 --- /dev/null +++ b/bless/request/bless_request_common.py @@ -0,0 +1,18 @@ +""" +.. module: bless.request.bless_request_common + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from marshmallow import ValidationError + +VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" +VALID_SSH_ED25519_PUBLIC_KEY_HEADER = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5" + + +def validate_ssh_public_key(public_key): + if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER) or public_key.startswith( + VALID_SSH_ED25519_PUBLIC_KEY_HEADER): + pass + # todo other key types + else: + raise ValidationError('Invalid SSH Public Key.') diff --git a/bless/request/bless_request_host.py b/bless/request/bless_request_host.py new file mode 100644 index 0000000..5729334 --- /dev/null +++ b/bless/request/bless_request_host.py @@ -0,0 +1,62 @@ +""" +.. module: bless.request.bless_request_host + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from enum import Enum + +from bless.config.bless_config import HOSTNAME_VALIDATION_OPTION, HOSTNAME_VALIDATION_DEFAULT +from bless.request.bless_request_common import validate_ssh_public_key +from marshmallow import Schema, fields, validates_schema, ValidationError, post_load, validates +from marshmallow.validate import URL + +HOSTNAME_VALIDATION_OPTIONS = Enum('HostNameValidationOptions', + 'url ' # Valid url format + 'disabled' # no validation + ) + + +def validate_hostname(hostname, hostname_validation): + if hostname_validation == HOSTNAME_VALIDATION_OPTIONS.disabled: + return + else: + validator = URL(require_tld=False, schemes='ssh', error='Invalid hostname "{input}".') + validator('ssh://{}'.format(hostname)) + + +class BlessHostSchema(Schema): + hostnames = fields.Str(required=True) + public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, data, original_data): + unknown = set(original_data) - set(self.fields) + if unknown: + raise ValidationError('Unknown field', unknown) + + @post_load + def make_bless_request(self, data): + return BlessHostRequest(**data) + + @validates('hostnames') + def validate_hostnames(self, hostnames): + if HOSTNAME_VALIDATION_OPTION in self.context: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[self.context[HOSTNAME_VALIDATION_OPTION]] + else: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[HOSTNAME_VALIDATION_DEFAULT] + for hostname in hostnames.split(','): + validate_hostname(hostname, hostname_validation) + + +class BlessHostRequest: + def __init__(self, hostnames, public_key_to_sign): + """ + A BlessRequest must have the following key value pairs to be valid. + :param hostnames: Comma-separated list of hostname(s) to include in this host certificate. + :param public_key_to_sign: The id_XXX.pub that will be used in the SSH request. This is enforced in the issued certificate. + """ + self.hostnames = hostnames + self.public_key_to_sign = public_key_to_sign + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/bless/request/bless_request_user.py b/bless/request/bless_request_user.py new file mode 100644 index 0000000..1e31d80 --- /dev/null +++ b/bless/request/bless_request_user.py @@ -0,0 +1,151 @@ +""" +.. module: bless.request.bless_request_user + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import re +from enum import Enum + +import ipaddress +from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ + USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT, REMOTE_USERNAMES_BLACKLIST_OPTION, \ + REMOTE_USERNAMES_BLACKLIST_DEFAULT +from bless.request.bless_request_common import validate_ssh_public_key +from marshmallow import Schema, fields, post_load, ValidationError, validates_schema +from marshmallow import validates +from marshmallow.validate import Email + +# man 8 useradd +USERNAME_PATTERN = re.compile(r'[a-z_][a-z0-9_-]*[$]?\Z') + +# debian +# On Debian, the only constraints are that usernames must neither start +# with a dash ('-') nor plus ('+') nor tilde ('~') nor contain a colon +# (':'), a comma (','), or a whitespace (space: ' ', end of line: '\n', +# tabulation: '\t', etc.). Note that using a slash ('/') may break the +# default algorithm for the definition of the user's home directory. +USERNAME_PATTERN_DEBIAN = re.compile(r'\A[^-+~][^:,\s]*\Z') + +# It appears that most printable ascii is valid, excluding whitespace, #, and commas. +# There doesn't seem to be any practical size limits of an SSH Certificate Principal (> 4096B allowed). +PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') + +USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', + 'useradd ' # Allowable usernames per 'man 8 useradd' + 'debian ' # Allowable usernames on debian systems. + 'email ' # username is a valid email address. + 'principal ' # SSH Certificate Principal. See 'man 5 sshd_config'. + 'disabled') # no additional validation of the string. + + +def validate_ips(ips): + try: + for ip in ips.split(','): + ipaddress.ip_network(ip, strict=True) + except ValueError: + raise ValidationError('Invalid IP address.') + + +def validate_user(user, username_validation, username_blacklist=None): + if username_blacklist: + if re.match(username_blacklist, user) is not None: + raise ValidationError('Username contains invalid characters.') + + if username_validation == USERNAME_VALIDATION_OPTIONS.disabled: + return + elif username_validation == USERNAME_VALIDATION_OPTIONS.email: + Email('Invalid email address.').__call__(user) + elif username_validation == USERNAME_VALIDATION_OPTIONS.principal: + _validate_principal(user) + elif len(user) > 32: + raise ValidationError('Username is too long.') + elif username_validation == USERNAME_VALIDATION_OPTIONS.useradd: + _validate_user_useradd(user) + elif username_validation == USERNAME_VALIDATION_OPTIONS.debian: + _validate_user_debian(user) + else: + raise ValidationError('Invalid username validator.') + + +def _validate_user_useradd(user): + if USERNAME_PATTERN.match(user) is None: + raise ValidationError('Username contains invalid characters.') + + +def _validate_user_debian(user): + if USERNAME_PATTERN_DEBIAN.match(user) is None: + raise ValidationError('Username contains invalid characters.') + + +def _validate_principal(principal): + if PRINCIPAL_PATTERN.match(principal) is None: + raise ValidationError('Principal contains invalid characters.') + + +class BlessUserSchema(Schema): + bastion_ips = fields.Str(validate=validate_ips, required=True) + bastion_user = fields.Str(required=True) + bastion_user_ip = fields.Str(validate=validate_ips, required=True) + command = fields.Str(required=True) + public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) + remote_usernames = fields.Str(required=True) + kmsauth_token = fields.Str(required=False) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, data, original_data): + unknown = set(original_data) - set(self.fields) + if unknown: + raise ValidationError('Unknown field', unknown) + + @post_load + def make_bless_request(self, data): + return BlessUserRequest(**data) + + @validates('bastion_user') + def validate_bastion_user(self, user): + if USERNAME_VALIDATION_OPTION in self.context: + username_validation = USERNAME_VALIDATION_OPTIONS[self.context[USERNAME_VALIDATION_OPTION]] + else: + username_validation = USERNAME_VALIDATION_OPTIONS[USERNAME_VALIDATION_DEFAULT] + validate_user(user, username_validation) + + @validates('remote_usernames') + def validate_remote_usernames(self, remote_usernames): + if REMOTE_USERNAMES_VALIDATION_OPTION in self.context: + username_validation = USERNAME_VALIDATION_OPTIONS[self.context[REMOTE_USERNAMES_VALIDATION_OPTION]] + else: + username_validation = USERNAME_VALIDATION_OPTIONS[REMOTE_USERNAMES_VALIDATION_DEFAULT] + if REMOTE_USERNAMES_BLACKLIST_OPTION in self.context: + username_blacklist = self.context[REMOTE_USERNAMES_BLACKLIST_OPTION] + else: + username_blacklist = REMOTE_USERNAMES_BLACKLIST_DEFAULT + for remote_username in remote_usernames.split(','): + validate_user(remote_username, username_validation, username_blacklist) + + +class BlessUserRequest: + def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, + remote_usernames, kmsauth_token=None): + """ + A BlessRequest must have the following key value pairs to be valid. + :param bastion_ips: The source IPs where the SSH connection will be initiated from. This is + enforced in the issued certificate. + :param bastion_user: The user on the bastion, who is initiating the SSH request. + :param bastion_user_ip: The IP of the user accessing the bastion. + :param command: Text information about the SSH request of the user. + :param public_key_to_sign: The id_XXX.pub that will be used in the SSH request. This is + enforced in the issued certificate. + :param remote_usernames: Comma-separated list of username(s) or authorized principals on the remote + server that will be used in the SSH request. This is enforced in the issued certificate. + :param kmsauth_token: An optional kms auth token to authenticate the user. + """ + self.bastion_ips = bastion_ips + self.bastion_user = bastion_user + self.bastion_user_ip = bastion_user_ip + self.command = command + self.public_key_to_sign = public_key_to_sign + self.remote_usernames = remote_usernames + self.kmsauth_token = kmsauth_token + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/bless/ssh/certificate_authorities/rsa_certificate_authority.py b/bless/ssh/certificate_authorities/rsa_certificate_authority.py index 385c77f..55ef165 100644 --- a/bless/ssh/certificate_authorities/rsa_certificate_authority.py +++ b/bless/ssh/certificate_authorities/rsa_certificate_authority.py @@ -29,8 +29,6 @@ def __init__(self, pem_private_key, private_key_password=None): private_key_password, default_backend()) - self.signer = self.private_key.signer(padding.PKCS1v15(), - hashes.SHA1()) ca_pub_numbers = self.private_key.public_key().public_numbers() self.e = ca_pub_numbers.e @@ -55,7 +53,6 @@ def sign(self, body): signature key. :return: SSH RSA Signature. """ - self.signer.update(body) - signature = self.signer.finalize() + signature = self.private_key.sign(body, padding.PKCS1v15(), hashes.SHA1()) return self._serialize_signature(signature) diff --git a/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py index f080767..25aea40 100644 --- a/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py +++ b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py @@ -12,12 +12,12 @@ def get_ssh_certificate_authority(private_key, password=None): """ Returns the proper SSHCertificateAuthority instance based off the private_key type. - :param private_key: SSH compatible Private Key (e.g., PEM or SSH Protocol 2 Private Key). + :param private_key: ASCII bytes of an SSH compatible Private Key (e.g., PEM or SSH Protocol 2 Private Key). It should be encrypted with a password, but that is not required. - :param password: Password to decrypt the Private Key, if it is encrypted. Which it should be. + :param password: ASCII bytes of the Password to decrypt the Private Key, if it is encrypted. Which it should be. :return: An SSHCertificateAuthority instance. """ - if private_key.startswith(SSHCertificateAuthorityPrivateKeyType.RSA): + if private_key.decode('ascii').startswith(SSHCertificateAuthorityPrivateKeyType.RSA): return RSACertificateAuthority(private_key, password) else: raise TypeError("Unsupported CA Private Key Type") diff --git a/bless/ssh/certificates/ed25519_certificate_builder.py b/bless/ssh/certificates/ed25519_certificate_builder.py new file mode 100644 index 0000000..8293980 --- /dev/null +++ b/bless/ssh/certificates/ed25519_certificate_builder.py @@ -0,0 +1,39 @@ +""" +.. module: bless.ssh.certificates.ed25519_certificate_builder + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.certificates.ssh_certificate_builder import \ + SSHCertificateBuilder, SSHCertifiedKeyType +from bless.ssh.protocol.ssh_protocol import pack_ssh_string + + +class ED25519CertificateBuilder(SSHCertificateBuilder): + def __init__(self, ca, cert_type, ssh_public_key_ed25519): + """ + Produces an SSH certificate for ED25519 public keys. + :param ca: The SSHCertificateAuthority that will sign the certificate. The + SSHCertificateAuthority type does not need to be the same type as the + SSHCertificateBuilder. + :param cert_type: The SSHCertificateType. Is this a User or Host certificate? Some of + the SSH Certificate fields do not apply or have a slightly different meaning depending on + the certificate type. + See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :param ssh_public_key_ed25519: The ED25519PublicKey to issue a certificate for. + """ + super(ED25519CertificateBuilder, self).__init__(ca, cert_type) + self.cert_key_type = SSHCertifiedKeyType.ED25519 + self.ssh_public_key = ssh_public_key_ed25519 + self.public_key_comment = ssh_public_key_ed25519.key_comment + self.a = ssh_public_key_ed25519.a + + def _serialize_ssh_public_key(self): + """ + Serialize the Public Key into a string. This is not specified in + http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + but https://tools.ietf.org/id/draft-ietf-curdle-ssh-ed25519-02.html + :return: The bytes that belong in the SSH Certificate between the nonce and the + certificate serial number. + """ + public_key = pack_ssh_string(self.a) + return public_key diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index cc1ffd7..8e4fb09 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -207,7 +207,7 @@ def get_cert_file(self, bypass_time_validity_check=False): file_contents = ( "{} {} {}" ).format(self.cert_key_type, - base64.b64encode(self._sign_cert(bypass_time_validity_check)), + str(base64.b64encode(self._sign_cert(bypass_time_validity_check)), encoding='ascii'), self.public_key_comment) return file_contents @@ -232,8 +232,8 @@ def _initialize_unset_attributes(self): if not self.public_key_comment: self.public_key_comment = \ - 'Certificate type:{} principals:{} with the id:[{}]'.format( - self.cert_type, self.valid_principals, self.key_id) + 'Certificate type[{}] principals[{}] with the id[{}]'.format( + self.cert_type, ','.join(self.valid_principals), self.key_id) def _validate_cert_properties(self): if self.valid_after >= self.valid_before: @@ -273,7 +273,7 @@ def _serialize_extensions(self): # sequence. Each named option may only appear once in a certificate. extensions_list = sorted(self.extensions) - serialized = '' + serialized = b'' # Format is a series of {extension name}{empty string} for extension in extensions_list: serialized += pack_ssh_string(extension) @@ -282,7 +282,7 @@ def _serialize_extensions(self): return serialized def _serialize_valid_principals(self): - serialized = '' + serialized = b'' for principal in self.valid_principals: serialized += pack_ssh_string(principal) @@ -292,7 +292,7 @@ def _serialize_valid_principals(self): def _serialize_critical_options(self): # Options must be lexically ordered by "name" if they appear in the # sequence. Each named option may only appear once in a certificate. - serialized = '' + serialized = b'' if self.critical_option_force_command is not None: serialized += pack_ssh_string('force-command') diff --git a/bless/ssh/certificates/ssh_certificate_builder_factory.py b/bless/ssh/certificates/ssh_certificate_builder_factory.py index e26d7b7..802c210 100644 --- a/bless/ssh/certificates/ssh_certificate_builder_factory.py +++ b/bless/ssh/certificates/ssh_certificate_builder_factory.py @@ -5,6 +5,8 @@ """ from bless.ssh.certificates.rsa_certificate_builder \ import RSACertificateBuilder +from bless.ssh.certificates.ed25519_certificate_builder \ + import ED25519CertificateBuilder from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key @@ -23,5 +25,7 @@ def get_ssh_certificate_builder(ca, cert_type, public_key_to_sign): if ssh_public_key.type is SSHPublicKeyType.RSA: return RSACertificateBuilder(ca, cert_type, ssh_public_key) + elif ssh_public_key.type is SSHPublicKeyType.ED25519: + return ED25519CertificateBuilder(ca, cert_type, ssh_public_key) else: raise TypeError("Unsupported Public Key Type") diff --git a/bless/ssh/protocol/ssh_protocol.py b/bless/ssh/protocol/ssh_protocol.py index 3cf853b..2fa37c9 100644 --- a/bless/ssh/protocol/ssh_protocol.py +++ b/bless/ssh/protocol/ssh_protocol.py @@ -44,13 +44,13 @@ def pack_ssh_string(string): :return: An SSH String stored as a unint32 representing the length of the input string, followed by that many bytes. """ - if isinstance(string, unicode): + if isinstance(string, str): string = string.encode('utf-8') str_len = len(string) if len(string) > 4294967295: - raise ValueError("String must be less than 2^32 bytes long") + raise ValueError("String must be less than 2^32 bytes long.") return struct.pack('>I{}s'.format(str_len), str_len, string) @@ -58,13 +58,13 @@ def pack_ssh_string(string): def pack_ssh_uint64(i): """ Packs a 64-bit unsigned integer. - :param i: integer or long. + :param i: integer :return: Eight bytes in the order of decreasing significance (network byte order). """ - if not isinstance(i, int) and not isinstance(i, long): - raise TypeError("Must be a int or long") + if not isinstance(i, int): + raise TypeError("Must be an int") elif i.bit_length() > 64: - raise ValueError("Must be a 64bit value") + raise ValueError("Must be a 64bit value.") return struct.pack('>Q', i) @@ -75,10 +75,10 @@ def pack_ssh_uint32(i): :param i: integer or long. :return: Four bytes in the order of decreasing significance (network byte order). """ - if not isinstance(i, int) and not isinstance(i, long): - raise TypeError("Must be a int or long") + if not isinstance(i, int): + raise TypeError("Must be an int") elif i.bit_length() > 32: - raise ValueError("Must be a 32bit value") + raise ValueError("Must be a 32bit value.") return struct.pack('>I', i) @@ -94,7 +94,7 @@ def _hex_characters_length(mpint): # how many bytes? num_bits = mpint.bit_length() - num_bytes = num_bits / 8 + num_bytes = num_bits // 8 # if there are remaining bits, we need an extra byte if num_bits % 8: diff --git a/bless/ssh/public_keys/ed25519_public_key.py b/bless/ssh/public_keys/ed25519_public_key.py new file mode 100644 index 0000000..5199e14 --- /dev/null +++ b/bless/ssh/public_keys/ed25519_public_key.py @@ -0,0 +1,63 @@ +""" +.. module: bless.ssh.public_keys.ed25519_public_key + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import hashlib + +from bless.ssh.public_keys.ssh_public_key import SSHPublicKey, SSHPublicKeyType +from cryptography.hazmat.primitives.serialization import ssh + + +class ED25519PublicKey(SSHPublicKey): + def __init__(self, ssh_public_key): + """ + Extracts the useful ED25519 Public Key information from an SSH Public Key file. + :param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-ed25519 AAAAB3NzaC1yc2E..'). + """ + super(ED25519PublicKey, self).__init__() + + self.type = SSHPublicKeyType.ED25519 + + split_ssh_public_key = ssh_public_key.split(' ') + split_key_len = len(split_ssh_public_key) + + # is there a key comment at the end? + if split_key_len > 2: + self.key_comment = ' '.join(split_ssh_public_key[2:]) + else: + self.key_comment = '' + + # hazmat does not support ed25519 so we have out own loader based on serialization.load_ssh_public_key + + if split_key_len < 2: + raise ValueError( + 'Key is not in the proper format or contains extra data.') + + key_type = split_ssh_public_key[0] + key_body = split_ssh_public_key[1] + + if key_type != SSHPublicKeyType.ED25519: + raise TypeError("Public Key is not the correct type or format") + + try: + decoded_data = base64.b64decode(key_body) + except TypeError: + raise ValueError('Key is not in the proper format.') + + inner_key_type, rest = ssh._ssh_read_next_string(decoded_data) + + if inner_key_type != key_type.encode("utf-8"): + raise ValueError( + 'Key header and key body contain different key type values.' + ) + + # ed25519 public key is a single string https://tools.ietf.org/html/rfc8032#section-5.1.5 + self.a, rest = ssh._ssh_read_next_string(rest) + + key_bytes = base64.b64decode(split_ssh_public_key[1]) + fingerprint = hashlib.md5(key_bytes).hexdigest() + + self.fingerprint = 'ED25519 ' + ':'.join( + fingerprint[i:i + 2] for i in range(0, len(fingerprint), 2)) diff --git a/bless/ssh/public_keys/rsa_public_key.py b/bless/ssh/public_keys/rsa_public_key.py index 58a0e61..d1afcbd 100644 --- a/bless/ssh/public_keys/rsa_public_key.py +++ b/bless/ssh/public_keys/rsa_public_key.py @@ -57,7 +57,7 @@ def __init__(self, ssh_public_key): else: self.key_comment = '' - public_key = serialization.load_ssh_public_key(ssh_public_key, default_backend()) + public_key = serialization.load_ssh_public_key(ssh_public_key.encode('ascii'), default_backend()) ca_pub_numbers = public_key.public_numbers() if not isinstance(ca_pub_numbers, RSAPublicNumbers): raise TypeError("Public Key is not the correct type or format") diff --git a/bless/ssh/public_keys/ssh_public_key_factory.py b/bless/ssh/public_keys/ssh_public_key_factory.py index 066c4bb..e9837e7 100644 --- a/bless/ssh/public_keys/ssh_public_key_factory.py +++ b/bless/ssh/public_keys/ssh_public_key_factory.py @@ -4,6 +4,7 @@ :license: Apache, see LICENSE for more details. """ from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType @@ -17,5 +18,8 @@ def get_ssh_public_key(ssh_public_key): rsa_public_key = RSAPublicKey(ssh_public_key) rsa_public_key.validate_for_signing() return rsa_public_key + elif ssh_public_key.startswith(SSHPublicKeyType.ED25519): + ed25519_public_key = ED25519PublicKey(ssh_public_key) + return ed25519_public_key else: raise TypeError("Unsupported Public Key Type") diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index 7770d86..b96f56e 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -32,23 +32,23 @@ obtained by appending -cert.pub to identity filenames" e.g. the . """ import json +import os import stat import sys import boto3 -import os def main(argv): if len(argv) < 9 or len(argv) > 10: - print ( + print( 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip ' 'remote_usernames bastion_ips bastion_command ' ' [kmsauth token]') return -1 region, lambda_function_name, bastion_user, bastion_user_ip, remote_usernames, bastion_ips, \ - bastion_command, public_key_filename, certificate_filename = argv + bastion_command, public_key_filename, certificate_filename = argv[:9] with open(public_key_filename, 'r') as f: public_key = f.read().strip() @@ -71,13 +71,13 @@ def main(argv): print('{}\n'.format(response['ResponseMetadata'])) if response['StatusCode'] != 200: - print ('Error creating cert.') + print('Error creating cert.') return -1 payload = json.loads(response['Payload'].read()) if 'certificate' not in payload: - print payload + print(payload) return -1 cert = payload['certificate'] diff --git a/bless_client/bless_client_host.py b/bless_client/bless_client_host.py new file mode 100644 index 0000000..94654a0 --- /dev/null +++ b/bless_client/bless_client_host.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +"""bless_client +A sample client to invoke the BLESS Host SSH Cert Lambda function and save the signed SSH Certificate. + +Usage: + bless_client_host.py region lambda_function_name hostnames + + region: AWS region where your lambda is deployed. + + lambda_function_name: The AWS Lambda function's alias or ARN to invoke. + + hostnames: Comma-separated list of hostname(s) to include in this host certificate. + + id_rsa.pub to sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + + output id_rsa-cert.pub: The file where the certificate should be saved. Per man SSH(1): + "ssh will also try to load certificate information from the filename + obtained by appending -cert.pub to identity filenames" e.g. the . +""" +import json +import os +import stat +import sys + +import boto3 + + +def main(argv): + if len(argv) != 5: + print( + 'Usage: bless_client_host.py region lambda_function_name hostnames ' + '') + print(len(argv)) + return -1 + + region, lambda_function_name, hostnames, public_key_filename, certificate_filename = argv + + with open(public_key_filename, 'r') as f: + public_key = f.read().strip() + + payload = {'hostnames': hostnames, 'public_key_to_sign': public_key} + + payload_json = json.dumps(payload) + + print('Executing:') + print('payload_json is: \'{}\''.format(payload_json)) + lambda_client = boto3.client('lambda', region_name=region) + response = lambda_client.invoke(FunctionName=lambda_function_name, + InvocationType='RequestResponse', LogType='None', + Payload=payload_json) + print('{}\n'.format(response['ResponseMetadata'])) + + if response['StatusCode'] != 200: + print('Error creating cert.') + return -1 + + payload = json.loads(response['Payload'].read()) + + if 'certificate' not in payload: + print(payload) + return -1 + + cert = payload['certificate'] + + with os.fdopen(os.open(certificate_filename, os.O_WRONLY | os.O_CREAT, 0o600), + 'w') as cert_file: + cert_file.write(cert) + + # If cert_file already existed with the incorrect permissions, fix them. + file_status = os.stat(certificate_filename) + if 0o600 != (file_status.st_mode & 0o777): + os.chmod(certificate_filename, stat.S_IRUSR | stat.S_IWUSR) + + print('Wrote Certificate to: ' + certificate_filename) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/lambda_compile.sh b/lambda_compile.sh new file mode 100755 index 0000000..dd35d83 --- /dev/null +++ b/lambda_compile.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +yum install -y python37 +python3.7 -m venv /tmp/venv +/tmp/venv/bin/pip install --upgrade pip setuptools +/tmp/venv/bin/pip install -e . +cp -r /tmp/venv/lib/python3.7/site-packages/. ./aws_lambda_libs +cp -r /tmp/venv/lib64/python3.7/site-packages/. ./aws_lambda_libs diff --git a/requirements.txt b/requirements.txt index 7c3db7b..6ff53c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,16 @@ -e . -boto3==1.4.4 -botocore==1.5.17 -cffi==1.9.1 -configparser==3.5.0 -coverage==4.3.4 -cryptography==1.7.2 -docutils==0.13.1 -enum34==1.1.6 -flake8==3.3.0 -futures==3.0.5 -idna==2.2 -ipaddress==1.0.18 -jmespath==0.9.1 -kmsauth==0.2.0 -marshmallow==2.13.0 -mccabe==0.6.1 -packaging==16.8 -py==1.4.32 -pyasn1==0.2.2 -pycodestyle==2.3.1 -pycparser==2.17 -pyflakes==1.5.0 -pyparsing==2.1.10 -pytest==3.0.6 -python-dateutil==2.6.0 -s3transfer==0.1.10 -six==1.10.0 \ No newline at end of file +asn1crypto==0.24.0 +boto3==1.9.151 +botocore==1.12.151 +cffi==1.12.3 +cryptography==2.6.1 +docutils==0.14 +ipaddress==1.0.22 +jmespath==0.9.4 +kmsauth==0.3.0 +marshmallow==2.19.2 +pycparser==2.19 +python-dateutil==2.8.0 +s3transfer==0.2.0 +six==1.12.0 +urllib3==1.24.3 diff --git a/setup.py b/setup.py index 0a3a3ae..f148b9d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ about = {} with open(os.path.join(ROOT, "bless", "__about__.py")) as f: - exec (f.read(), about) + exec(f.read(), about) setup( name=about["__title__"], @@ -19,8 +19,6 @@ packages=find_packages(exclude=["test*"]), install_requires=[ 'boto3', - 'botocore', - 'cffi', 'cryptography', 'ipaddress', 'marshmallow', @@ -31,7 +29,8 @@ 'coverage', 'flake8', 'pyflakes', - 'pytest' + 'pytest', + 'pytest-mock' ] } ) diff --git a/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg new file mode 100644 index 0000000..28b2830 --- /dev/null +++ b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg @@ -0,0 +1,33 @@ +[Bless CA] +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu,alloweduser + +# todo get from config, with some sane defaults +#[loggers] +#keys=root +# +#[handlers] +#keys=stream_handler +# +#[formatters] +#keys=formatter +# +#[logger_root] +#level=INFO +#handlers=stream_handler +# +#[handler_stream_handler] +#class=StreamHandler +#level=DEBUG +#formatter=formatter +#args=(sys.stderr,) +# +#[formatter_formatter] +#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg b/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg new file mode 100644 index 0000000..7f77cba --- /dev/null +++ b/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg @@ -0,0 +1,35 @@ +[Bless CA] +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu,alloweduser +kmsauth_validate_remote_usernames_against_iam_groups = True +kmsauth_iam_group_name_format = ssh-{} + +# todo get from config, with some sane defaults +#[loggers] +#keys=root +# +#[handlers] +#keys=stream_handler +# +#[formatters] +#keys=formatter +# +#[logger_root] +#level=INFO +#handlers=stream_handler +# +#[handler_stream_handler] +#class=StreamHandler +#level=DEBUG +#formatter=formatter +#args=(sys.stderr,) +# +#[formatter_formatter] +#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/tests/aws_lambda/only-use-for-unit-tests.pem.bz2 b/tests/aws_lambda/only-use-for-unit-tests.pem.bz2 new file mode 100644 index 0000000..b8cbee7 Binary files /dev/null and b/tests/aws_lambda/only-use-for-unit-tests.pem.bz2 differ diff --git a/tests/aws_lambda/only-use-for-unit-tests.zlib b/tests/aws_lambda/only-use-for-unit-tests.zlib new file mode 100644 index 0000000..db65cf1 Binary files /dev/null and b/tests/aws_lambda/only-use-for-unit-tests.zlib differ diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py deleted file mode 100644 index 2b7f731..0000000 --- a/tests/aws_lambda/test_bless_lambda.py +++ /dev/null @@ -1,215 +0,0 @@ -import json -import os - -import pytest - -from bless.aws_lambda.bless_lambda import lambda_handler -from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ - EXAMPLE_ED25519_PUBLIC_KEY - - -class Context(object): - aws_request_id = 'bogus aws_request_id' - invoked_function_arn = 'bogus invoked_function_arn' - - -VALID_TEST_REQUEST = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" -} - -INVALID_TEST_REQUEST = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "invalid_ip", - "bastion_user": "user", - "bastion_user_ip": "invalid_ip" -} - -VALID_TEST_REQUEST_KMSAUTH = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1", - "kmsauth_token": "validkmsauthtoken", -} - -INVALID_TEST_REQUEST_KEY_TYPE = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" -} - -INVALID_TEST_REQUEST_EXTRA_FIELD = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1", - "bastion_ip": "127.0.0.1" # Note this is now an invalid field. -} - -INVALID_TEST_REQUEST_MISSING_FIELD = { - "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" -} - -VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { - "remote_usernames": "user1,user2", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" -} - -INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { - "remote_usernames": ",user#", - "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" -} - -os.environ['AWS_REGION'] = 'us-west-2' - - -def test_basic_local_request(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_basic_local_unused_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_basic_local_missing_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test-kmsauth.cfg')) - assert output['errorType'] == 'InputValidationError' - - -def test_invalid_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test-kmsauth.cfg')) - assert output['errorType'] == 'KMSAuthValidationError' - - -def test_invalid_request(): - output = lambda_handler(INVALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['errorType'] == 'InputValidationError' - - -def test_local_request_key_not_found(): - with pytest.raises(IOError): - lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test-broken.cfg')) - - -def test_local_request_config_not_found(): - with pytest.raises(ValueError): - lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'none')) - - -def test_local_request_invalid_pub_key(): - output = lambda_handler(INVALID_TEST_REQUEST_KEY_TYPE, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['errorType'] == 'InputValidationError' - - -def test_local_request_extra_field(): - output = lambda_handler(INVALID_TEST_REQUEST_EXTRA_FIELD, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['errorType'] == 'InputValidationError' - - -def test_local_request_missing_field(): - output = lambda_handler(INVALID_TEST_REQUEST_MISSING_FIELD, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['errorType'] == 'InputValidationError' - - -def test_local_request_with_test_user(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test-with-test-user.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_local_request_with_custom_certificate_extensions(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test-with-certificate-extensions.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_local_request_with_empty_certificate_extensions(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test-with-certificate-extensions-empty.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_local_request_with_multiple_principals(): - output = lambda_handler(VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - -def test_invalid_request_with_multiple_principals(): - output = lambda_handler(INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), - 'bless-test.cfg')) - assert output['errorType'] == 'InputValidationError' diff --git a/tests/aws_lambda/test_bless_lambda_host.py b/tests/aws_lambda/test_bless_lambda_host.py new file mode 100644 index 0000000..9797f17 --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda_host.py @@ -0,0 +1,53 @@ +import os + +from bless.aws_lambda.bless_lambda_host import lambda_handler_host +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD + + +class Context(object): + aws_request_id = 'bogus aws_request_id' + invoked_function_arn = 'bogus invoked_function_arn' + + +VALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com", +} + +VALID_TEST_REQUEST_MULTIPLE_HOSTS = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com,thatthis.com", +} + +INVALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostname": "thisthat.com", # Wrong key name +} + +os.environ['AWS_REGION'] = 'us-west-2' + + +def test_basic_local_request(): + output = lambda_handler_host(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + print(output) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_request_with_multiple_hosts(): + output = lambda_handler_host(VALID_TEST_REQUEST_MULTIPLE_HOSTS, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + print(output) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_request(): + output = lambda_handler_host(INVALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' diff --git a/tests/aws_lambda/test_bless_lambda_user.py b/tests/aws_lambda/test_bless_lambda_user.py new file mode 100644 index 0000000..40dce14 --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda_user.py @@ -0,0 +1,558 @@ +import os +import zlib + +import pytest + +from bless.aws_lambda.bless_lambda_user import lambda_handler_user +from bless.aws_lambda.bless_lambda import lambda_handler +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ + EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY + + +class Context(object): + aws_request_id = 'bogus aws_request_id' + invoked_function_arn = 'bogus invoked_function_arn' + + +VALID_TEST_REQUEST = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +VALID_TEST_REQUEST_ED2551 = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD = { + "remote_usernames": "user,anotheruser", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "someone@example.com", + "bastion_user_ip": "127.0.0.1" +} + +VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED = { + "remote_usernames": "'~:, \n\t@'", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "a33characterusernameyoumustbenuts", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "invalid_ip", + "bastion_user": "user", + "bastion_user_ip": "invalid_ip" +} + +VALID_TEST_REQUEST_KMSAUTH = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken", +} + +INVALID_TEST_REQUEST_KEY_TYPE = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_ECDSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST_EXTRA_FIELD = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1", + "bastion_ip": "127.0.0.1" # Note this is now an invalid field. +} + +INVALID_TEST_REQUEST_MISSING_FIELD = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { + "remote_usernames": "user1,user2", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { + "remote_usernames": ",user#", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST_USERNAME_INVALID = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "~@.", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE = { + "remote_usernames": "userb", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + +INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { + "remote_usernames": "root", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + +VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { + "remote_usernames": "alloweduser", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + +INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME = { + "remote_usernames": "alloweduser,balrog", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + + +def test_basic_local_request_with_wrapper(): + output = lambda_handler(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_request(): + output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_request_ed2551(): + output = lambda_handler_user(VALID_TEST_REQUEST_ED2551, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['certificate'].startswith('ssh-ed25519-cert-v01@openssh.com ') + + +def test_basic_local_unused_kmsauth_request(): + output = lambda_handler_user(VALID_TEST_REQUEST_KMSAUTH, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_missing_kmsauth_request(): + output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_basic_local_username_validation_disabled(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_options_username_validation': 'disabled', + 'bless_options_remote_usernames_validation': 'disabled', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_username_validation_email_remote_usernames_useradd(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_ca_private_key_file_bz2(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem.bz2', + 'bless_ca_ca_private_key_compression': 'bz2', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_ca_private_key_env_bz2(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key': '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', + 'bless_ca_ca_private_key_compression': 'bz2', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_ca_private_key_file_zlib(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.zlib', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_ca_private_key_env_zlib(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key': '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', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_ca_private_key_file_none_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_compression': 'none', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_uncompressed_with_zlib(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + with pytest.raises(zlib.error): + lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + + +def test_invalid_uncompressed_with_bz2(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_compression': 'bz2', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + with pytest.raises(OSError): + lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + + +def test_invalid_username_request(): + output = lambda_handler_user(INVALID_TEST_REQUEST_USERNAME_INVALID, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_invalid_kmsauth_request(): + output = lambda_handler_user(VALID_TEST_REQUEST_KMSAUTH, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + + +def test_invalid_request(): + output = lambda_handler_user(INVALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_local_request_key_not_found(): + with pytest.raises(IOError): + lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test-broken.cfg')) + + +def test_local_request_config_not_found(): + with pytest.raises(ValueError): + lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'none')) + + +def test_local_request_invalid_pub_key(): + output = lambda_handler_user(INVALID_TEST_REQUEST_KEY_TYPE, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_local_request_extra_field(): + output = lambda_handler_user(INVALID_TEST_REQUEST_EXTRA_FIELD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_local_request_missing_field(): + output = lambda_handler_user(INVALID_TEST_REQUEST_MISSING_FIELD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_local_request_with_test_user(): + output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test-with-test-user.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_custom_certificate_extensions(): + output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-with-certificate-extensions.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_empty_certificate_extensions(): + output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-with-certificate-extensions-empty.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_multiple_principals(): + output = lambda_handler_user(VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_request_with_multiple_principals(): + output = lambda_handler_user(INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' + + +def test_invalid_request_with_mismatched_bastion_and_remote(): + ''' + Test default kmsauth behavior, that a bastion_user and remote_usernames must match + :return: + ''' + output = lambda_handler_user(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + + +def test_invalid_request_with_unallowed_remote(): + output = lambda_handler_user(INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth-different-remote.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + + +def test_valid_request_with_allowed_remote(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth-different-remote.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_valid_request_with_allowed_remote_and_allowed_iam_group(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + clientmock = mocker.MagicMock() + clientmock.list_groups_for_user.return_value = {"Groups": [{"GroupName": "ssh-alloweduser"}]} + botomock = mocker.patch('boto3.client') + botomock.return_value = clientmock + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth-iam-group-validation.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_request_with_allowed_remote_and_not_allowed_iam_group(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + clientmock = mocker.MagicMock() + clientmock.list_groups_for_user.return_value = {"Groups": [{"GroupName": "ssh-notalloweduser"}]} + botomock = mocker.patch('boto3.client') + botomock.return_value = clientmock + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth-iam-group-validation.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + + +def test_basic_local_request_blacklisted(monkeypatch): + extra_environment_variables = { + 'bless_options_remote_usernames_blacklist': 'root|balrog', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler_user(INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) + assert output['errorType'] == 'InputValidationError' diff --git a/tests/config/full-with-kmsauth.cfg b/tests/config/full-with-kmsauth.cfg new file mode 100644 index 0000000..cc4a832 --- /dev/null +++ b/tests/config/full-with-kmsauth.cfg @@ -0,0 +1,22 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +username_validation = debian + +[Bless CA] +us-east-1_password = +us-west-2_password = +ca_private_key_file = +ca_private_key_compression = zlib + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu,alloweduser +kmsauth_validate_remote_usernames_against_iam_groups = False +kmsauth_iam_group_name_format = ssh-{} \ No newline at end of file diff --git a/tests/config/full-zlib.cfg b/tests/config/full-zlib.cfg new file mode 100644 index 0000000..e41e088 --- /dev/null +++ b/tests/config/full-zlib.cfg @@ -0,0 +1,17 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +server_certificate_validity_before_seconds = 4 +server_certificate_validity_after_seconds = 5 +logging_level = DEBUG +username_validation = debian +hostname_validation = disabled + +[Bless CA] +us-east-1_password = +us-west-2_password = +ca_private_key = +ca_private_key_compression = zlib diff --git a/tests/config/full.cfg b/tests/config/full.cfg index e5fc965..1ad8734 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -4,9 +4,14 @@ certificate_validity_after_seconds = 1 certificate_validity_before_seconds = 1 entropy_minimum_bits = 2 random_seed_bytes = 3 +server_certificate_validity_before_seconds = 4 +server_certificate_validity_after_seconds = 5 logging_level = DEBUG +username_validation = debian +hostname_validation = disabled [Bless CA] us-east-1_password = us-west-2_password = ca_private_key_file = +ca_private_key_compression = zlib diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 68a7dff..70e907c 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -1,15 +1,38 @@ import base64 import os +import zlib import pytest - -from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ - ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ - CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, \ - LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION, BLESS_CA_SECTION, \ - CA_PRIVATE_KEY_FILE_OPTION, KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_KEY_ID_OPTION, \ - KMSAUTH_SERVICE_ID_OPTION, CERTIFICATE_EXTENSIONS_OPTION +from bless.config.bless_config import BlessConfig, \ + BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + ENTROPY_MINIMUM_BITS_OPTION, \ + RANDOM_SEED_BYTES_OPTION, \ + CERTIFICATE_VALIDITY_SEC_DEFAULT, \ + ENTROPY_MINIMUM_BITS_DEFAULT, \ + RANDOM_SEED_BYTES_DEFAULT, \ + LOGGING_LEVEL_DEFAULT, \ + LOGGING_LEVEL_OPTION, \ + BLESS_CA_SECTION, \ + CA_PRIVATE_KEY_FILE_OPTION, \ + KMSAUTH_SECTION, \ + KMSAUTH_USEKMSAUTH_OPTION, \ + KMSAUTH_KEY_ID_OPTION, \ + KMSAUTH_SERVICE_ID_OPTION, \ + CERTIFICATE_EXTENSIONS_OPTION, \ + USERNAME_VALIDATION_OPTION, \ + USERNAME_VALIDATION_DEFAULT, \ + REMOTE_USERNAMES_VALIDATION_OPTION, \ + CA_PRIVATE_KEY_COMPRESSION_OPTION, \ + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, \ + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, \ + HOSTNAME_VALIDATION_OPTION, \ + HOSTNAME_VALIDATION_DEFAULT, \ + VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION def test_empty_config(): @@ -21,33 +44,107 @@ def test_config_no_password(): with pytest.raises(ValueError) as e: BlessConfig('bogus-region', config_file=os.path.join(os.path.dirname(__file__), 'full.cfg')) - assert 'No Region Specific And No Default Password Provided.' == e.value.message + assert 'No Region Specific And No Default Password Provided.' == str(e.value) config = BlessConfig('bogus-region', - config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg')) + config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg')) assert '' == config.getpassword() +def test_wrong_compression_env_key(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'lzh', + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii') + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + # Create an empty config, everything is set in the environment + config = BlessConfig('us-east-1', config_file='') + + with pytest.raises(ValueError) as e: + config.getprivatekey() + + assert "Compression lzh is not supported." == str(e.value) + + +def test_none_compression_env_key(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'none', + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii') + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + # Create an empty config, everything is set in the environment + config = BlessConfig('us-east-1', config_file='') + + assert b'' == config.getprivatekey() + + +def test_zlib_positive_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_ca_ca_private_key': str(base64.b64encode(zlib.compress(b'')), + encoding='ascii') + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + # Create an empty config, everything is set in the environment + config = BlessConfig('us-east-1', config_file='') + + assert b'' == config.getprivatekey() + + +def test_zlib_compression_env_with_uncompressed_key(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii'), + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + # Create an empty config, everything is set in the environment + config = BlessConfig('us-east-1', config_file='') + + with pytest.raises(zlib.error) as e: + config.getprivatekey() + + def test_config_environment_override(monkeypatch): extra_environment_variables = { 'bless_options_certificate_validity_after_seconds': '1', 'bless_options_certificate_validity_before_seconds': '1', + 'bless_options_server_certificate_validity_after_seconds': '1', + 'bless_options_server_certificate_validity_before_seconds': '1', + 'bless_options_hostname_validation': 'disabled', 'bless_options_entropy_minimum_bits': '2', 'bless_options_random_seed_bytes': '3', 'bless_options_logging_level': 'DEBUG', 'bless_options_certificate_extensions': 'permit-X11-forwarding', + 'bless_options_username_validation': 'debian', + 'bless_options_remote_usernames_validation': 'useradd', 'bless_ca_us_east_1_password': '', 'bless_ca_default_password': '', 'bless_ca_ca_private_key_file': '', - 'bless_ca_ca_private_key': base64.b64encode(''), + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii'), 'kms_auth_use_kmsauth': 'True', 'kms_auth_kmsauth_key_id': '', 'kms_auth_kmsauth_serviceid': 'bless-test', } - for k,v in extra_environment_variables.items(): + for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) # Create an empty config, everything is set in the environment @@ -55,14 +152,19 @@ def test_config_environment_override(monkeypatch): assert 1 == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) assert 1 == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + assert 1 == config.getint(BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + assert 1 == config.getint(BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) assert 2 == config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION) assert 3 == config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION) assert 'DEBUG' == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) assert 'permit-X11-forwarding' == config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) + assert 'debian' == config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) + assert 'disabled' == config.get(BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION) + assert 'useradd' == config.get(BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_VALIDATION_OPTION) assert '' == config.getpassword() assert '' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) - assert '' == config.getprivatekey() + assert b'' == config.getprivatekey() assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION) assert '' == config.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION) @@ -73,30 +175,70 @@ def test_config_environment_override(monkeypatch): @pytest.mark.parametrize( - "config,region,expected_cert_valid,expected_entropy_min,expected_rand_seed,expected_log_level," - "expected_password", [ + "config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, " + "expected_host_cert_before_valid, expected_host_cert_after_valid, " + "expected_log_level, expected_password, expected_username_validation, " + "expected_hostname_validation, expected_key_compression", + [ ((os.path.join(os.path.dirname(__file__), 'minimal.cfg')), 'us-west-2', - CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, + CERTIFICATE_VALIDITY_SEC_DEFAULT, + ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, LOGGING_LEVEL_DEFAULT, - ''), - ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-west-2', - 1, 2, 3, 'DEBUG', - ''), + '', + USERNAME_VALIDATION_DEFAULT, + HOSTNAME_VALIDATION_DEFAULT, + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT + ), + ((os.path.join(os.path.dirname(__file__), 'full-zlib.cfg')), 'us-west-2', + 1, 2, 3, 4, 5, 'DEBUG', + '', + 'debian', + 'disabled', + 'zlib' + ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', - 1, 2, 3, 'DEBUG', - '') + 1, 2, 3, 4, 5, 'DEBUG', + '', + 'debian', + 'disabled', + 'zlib' + ) ]) def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, - expected_log_level, expected_password): + expected_host_cert_before_valid, expected_host_cert_after_valid, + expected_log_level, expected_password, expected_username_validation, + expected_hostname_validation, expected_key_compression): config = BlessConfig(region, config_file=config) assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) - assert expected_entropy_min == config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION) assert expected_rand_seed == config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION) + assert expected_host_cert_before_valid == config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + assert expected_host_cert_after_valid == config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) assert expected_log_level == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) assert expected_password == config.getpassword() + assert expected_username_validation == config.get(BLESS_OPTIONS_SECTION, + USERNAME_VALIDATION_OPTION) + assert expected_hostname_validation == config.get(BLESS_OPTIONS_SECTION, + HOSTNAME_VALIDATION_OPTION) + assert expected_key_compression == config.get(BLESS_CA_SECTION, + CA_PRIVATE_KEY_COMPRESSION_OPTION) + + +def test_kms_config_opts(monkeypatch): + # Default option + config = BlessConfig("us-east-1", config_file=os.path.join(os.path.dirname(__file__), 'full.cfg')) + assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION) is False + + # Config file value + config = BlessConfig("us-east-1", config_file=os.path.join(os.path.dirname(__file__), 'full-with-kmsauth.cfg')) + assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION) is True + assert config.getboolean(KMSAUTH_SECTION, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION) is False diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py deleted file mode 100644 index e09e66a..0000000 --- a/tests/request/test_bless_request.py +++ /dev/null @@ -1,66 +0,0 @@ -import pytest -from bless.request.bless_request import validate_ips, validate_user, validate_principals -from marshmallow import ValidationError - - -def test_validate_ips(): - validate_ips(u'127.0.0.1') - with pytest.raises(ValidationError): - validate_ips(u'256.0.0.0') - validate_ips(u'127.0.0.1,172.1.1.1') - with pytest.raises(ValidationError): - validate_ips(u'256.0.0.0,172.1.1.1') - - -def test_validate_ips_cidr(): - validate_ips(u'10.0.0.0/8,172.1.1.1') - with pytest.raises(ValidationError): - validate_ips(u'10.10.10.10/8') - - -def test_validate_user_too_long(): - with pytest.raises(ValidationError) as e: - validate_user('a33characterusernameyoumustbenuts') - assert e.value.message == 'Username is too long.' - - -@pytest.mark.parametrize("test_input", [ - ('user#invalid'), - ('$userinvalid'), - ('userinvali$d'), - ('userin&valid'), - (' userinvalid') -]) -def test_validate_user_contains_junk(test_input): - with pytest.raises(ValidationError) as e: - validate_user(test_input) - assert e.value.message == 'Username contains invalid characters.' - - -@pytest.mark.parametrize("test_input", [ - ('uservalid'), - ('a32characterusernameyoumustok$'), - ('_uservalid$'), - ('abc123_-valid') -]) -def test_validate_user(test_input): - validate_user(test_input) - -@pytest.mark.parametrize("test_input", [ - ('uservalid'), - ('uservalid,uservalid2'), - ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,uservalid2') -]) -def test_validate_multiple_principals(test_input): - validate_principals(test_input) - -@pytest.mark.parametrize("test_input", [ - ('user invalid'), - ('uservalid,us#erinvalid2'), - ('uservalid,,uservalid2'), - (' uservalid'), -]) -def test_validate_multiple_principals(test_input): - with pytest.raises(ValidationError) as e: - validate_principals(test_input) - assert e.value.message == 'Principal contains invalid characters.' diff --git a/tests/request/test_bless_request_host.py b/tests/request/test_bless_request_host.py new file mode 100644 index 0000000..2846f61 --- /dev/null +++ b/tests/request/test_bless_request_host.py @@ -0,0 +1,50 @@ +import pytest +from bless.request.bless_request_host import HOSTNAME_VALIDATION_OPTIONS, BlessHostSchema, validate_hostname +from marshmallow import ValidationError + + +@pytest.mark.parametrize("test_input", [ + 'thisthat', + 'this.that', + '10.1.1.1' +]) +def test_validate_hostnames(test_input): + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.url) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + ['thisthat'], + 'this!that.com' +]) +def test_invalid_hostnames(test_input): + with pytest.raises(ValidationError) as e: + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.url) + assert str(e.value) == 'Invalid hostname "ssh://{}".'.format(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + ['thisthat'], + 'this!that.com', + 'this,that' +]) +def test_invalid_hostnames_with_disabled(test_input): + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.disabled) + + +@pytest.mark.parametrize("test_input", [ + 'thisthat,this.that,10.1.1.1', + 'this.that,thishostname' +]) +def test_valid_multiple_hostnames(test_input): + BlessHostSchema().validate_hostnames(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'thisthat, this.that', +]) +def test_invalid_multiple_hostnames(test_input): + with pytest.raises(ValidationError) as e: + BlessHostSchema().validate_hostnames(test_input) + assert str(e.value) == 'Invalid hostname "ssh:// this.that".' diff --git a/tests/request/test_bless_request_user.py b/tests/request/test_bless_request_user.py new file mode 100644 index 0000000..511b35b --- /dev/null +++ b/tests/request/test_bless_request_user.py @@ -0,0 +1,176 @@ +import pytest +from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ + REMOTE_USERNAMES_BLACKLIST_OPTION +from bless.request.bless_request_user import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessUserSchema +from marshmallow import ValidationError + + +def test_validate_ips(): + validate_ips('127.0.0.1') + with pytest.raises(ValidationError): + validate_ips('256.0.0.0') + validate_ips('127.0.0.1,172.1.1.1') + with pytest.raises(ValidationError): + validate_ips('256.0.0.0,172.1.1.1') + + +def test_validate_ips_cidr(): + validate_ips('10.0.0.0/8,172.1.1.1') + with pytest.raises(ValidationError): + validate_ips('10.10.10.10/8') + + +def test_validate_user_too_long(): + with pytest.raises(ValidationError) as e: + validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.useradd) + assert str(e.value) == 'Username is too long.' + + +@pytest.mark.parametrize("test_input", [ + ('user#invalid'), + ('$userinvalid'), + ('userinvali$d'), + ('userin&valid'), + (' userinvalid') +]) +def test_validate_user_contains_junk(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.useradd) + assert str(e.value) == 'Username contains invalid characters.' + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('a32characterusernameyoumustok$'), + ('_uservalid$'), + ('abc123_-valid') +]) +def test_validate_user(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.useradd) + + +def test_validate_user_debian_too_long(): + with pytest.raises(ValidationError) as e: + validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.debian) + assert str(e.value) == 'Username is too long.' + + +@pytest.mark.parametrize("test_input", [ + ('~userinvalid'), + ('-userinvalid'), + ('+userinvalid'), + ('user:invalid'), + ('user,invalid'), + ('user invalid'), + ('user\tinvalid'), + ('user\ninvalid') +]) +def test_validate_user_debian_invalid(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) + assert str(e.value) == 'Username contains invalid characters.' + + +@pytest.mark.parametrize("test_input", [ + ('root'), + ("admin"), + ("administrator"), + ('balrog'), + ("teal'c") +]) +def test_validate_user_blacklist(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.principal, 'root|admin.*|balrog|.+\'.*') + assert str(e.value) == 'Username contains invalid characters.' + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('a32characterusernameyoumustok$'), + ('_uservalid$'), + ('abc123_-valid'), + ('user~valid'), + ('user-valid'), + ('user+valid'), +]) +def test_validate_user_debian(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('a32characterusernameyoumustok$'), + ('!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~') +]) +def test_validate_user_principal(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.principal) + + +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts@example.com'), + ('a@example.com'), + ('a+b@example.com') +]) +def test_validate_user_email(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) + + +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts@ex@mple.com'), + ('a@example'), +]) +def test_invalid_user_email(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) + assert str(e.value) == 'Invalid email address.' + + +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts'), + ('~:, \n\t@'), + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~,'), +]) +def test_validate_user_disabled(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.disabled) + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('uservalid,uservalid2'), + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~,' + 'uservalid2') +]) +def test_validate_multiple_principals(test_input): + BlessUserSchema().validate_remote_usernames(test_input) + + schema = BlessUserSchema() + schema.context[USERNAME_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name + schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name + schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = 'balrog' + schema.validate_remote_usernames(test_input) + + +@pytest.mark.parametrize("test_input", [ + ('user invalid'), + ('uservalid,us#erinvalid2'), + ('uservalid,,uservalid2'), + (' uservalid'), + ('user\ninvalid'), + ('~:, \n\t@') +]) +def test_invalid_multiple_principals(test_input): + with pytest.raises(ValidationError) as e: + BlessUserSchema().validate_remote_usernames(test_input) + assert str(e.value) == 'Principal contains invalid characters.' + + +def test_invalid_user_with_default_context_of_useradd(): + with pytest.raises(ValidationError) as e: + BlessUserSchema().validate_bastion_user('user#invalid') + assert str(e.value) == 'Username contains invalid characters.' + + +def test_invalid_call_of_validate_user(): + with pytest.raises(ValidationError) as e: + validate_user('test', None) + assert str(e.value) == 'Invalid username validator.' diff --git a/tests/ssh/test_ssh_certificate_authority_factory.py b/tests/ssh/test_ssh_certificate_authority_factory.py index 9aad05c..bf7b6aa 100644 --- a/tests/ssh/test_ssh_certificate_authority_factory.py +++ b/tests/ssh/test_ssh_certificate_authority_factory.py @@ -29,14 +29,14 @@ def test_valid_key_missing_password(): def test_valid_key_invalid_password(): with pytest.raises(ValueError): - get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, 'bogus') + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, b'bogus') def test_valid_key_not_encrypted_invalid_pass(): with pytest.raises(TypeError): - get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED, 'bogus') + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED, b'bogus') def test_invalid_key(): with pytest.raises(TypeError): - get_ssh_certificate_authority('bogus') + get_ssh_certificate_authority(b'bogus') diff --git a/tests/ssh/test_ssh_certificate_builder_factory.py b/tests/ssh/test_ssh_certificate_builder_factory.py index a9ce14b..e35c9f8 100644 --- a/tests/ssh/test_ssh_certificate_builder_factory.py +++ b/tests/ssh/test_ssh_certificate_builder_factory.py @@ -4,6 +4,7 @@ get_ssh_certificate_authority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder, \ SSHCertifiedKeyType +from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ @@ -18,10 +19,12 @@ def test_valid_rsa_request(): assert cert.startswith(SSHCertifiedKeyType.RSA) -def test_invalid_ed25519_request(): - with pytest.raises(TypeError): - ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) - get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY) +def test_valid_ed25519_request(): + ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY) + cert = cert_builder.get_cert_file() + assert isinstance(cert_builder, ED25519CertificateBuilder) + assert cert.startswith(SSHCertifiedKeyType.ED25519) def test_invalid_key_request(): diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index 6c2d439..ee7033a 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -1,12 +1,14 @@ import base64 import pytest -from cryptography.hazmat.primitives.serialization import _ssh_read_next_string +from cryptography.hazmat.primitives.serialization.ssh import _ssh_read_next_string from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder +from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION, RSA_USER_CERT_MINIMAL, \ RSA_USER_CERT_DEFAULTS, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT, \ @@ -14,7 +16,8 @@ RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS, \ RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID, RSA_HOST_CERT_MANY_PRINCIPALS_KEY_ID, \ RSA_USER_CERT_MANY_PRINCIPALS_KEY_ID, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID, \ - RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS + RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS, \ + EXAMPLE_ED25519_PUBLIC_KEY, ED25519_USER_CERT_DEFAULTS, ED25519_USER_CERT_DEFAULTS_KEY_ID USER1 = 'user1' @@ -68,7 +71,7 @@ def test_serialize_no_principals(): cert = get_basic_cert_builder_rsa() assert list() == cert.valid_principals - assert '' == cert._serialize_valid_principals() + assert b'' == cert._serialize_valid_principals() def test_serialize_one_principal(): @@ -96,14 +99,14 @@ def test_no_extensions(): assert cert_builder.extensions is None cert_builder.clear_extensions() - assert '' == cert_builder._serialize_extensions() + assert b'' == cert_builder._serialize_extensions() def test_bogus_cert_validity_range(): cert_builder = get_basic_cert_builder_rsa() with pytest.raises(ValueError): + cert_builder.set_valid_before(99) cert_builder.set_valid_after(100) - cert_builder.set_valid_after(99) cert_builder._validate_cert_properties() @@ -139,7 +142,6 @@ def test_add_extensions(): for extension in extensions: cert_builder.add_extension(extension) - print base64.b64encode(cert_builder._serialize_extensions()) assert SSH_CERT_CUSTOM_EXTENSIONS == cert_builder._serialize_extensions() @@ -219,3 +221,15 @@ def test_nonce(): cert_builder2.set_nonce() assert cert_builder.nonce != cert_builder2.nonce + + +def test_ed25519_user_cert_defaults(): + ca = get_basic_rsa_ca() + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY) + cert_builder = ED25519CertificateBuilder(ca, SSHCertificateType.USER, pub_key) + cert_builder.set_nonce( + nonce=extract_nonce_from_cert(ED25519_USER_CERT_DEFAULTS)) + cert_builder.set_key_id(ED25519_USER_CERT_DEFAULTS_KEY_ID) + + cert = cert_builder.get_cert_file() + assert ED25519_USER_CERT_DEFAULTS == cert diff --git a/tests/ssh/test_ssh_protocol.py b/tests/ssh/test_ssh_protocol.py index 79530d9..84f92b1 100644 --- a/tests/ssh/test_ssh_protocol.py +++ b/tests/ssh/test_ssh_protocol.py @@ -1,65 +1,66 @@ +import binascii import pytest from bless.ssh.protocol.ssh_protocol import pack_ssh_mpint, _hex_characters_length, \ pack_ssh_uint32, pack_ssh_uint64, pack_ssh_string def test_strings(): - strings = {'': '00000000'.decode('hex'), u'abc': '00000003616263'.decode('hex'), - b'1234': '0000000431323334'.decode('hex'), '1234': '0000000431323334'.decode('hex')} + strings = {'': binascii.unhexlify('00000000'), 'abc': binascii.unhexlify('00000003616263'), + b'1234': binascii.unhexlify('0000000431323334'), '1234': binascii.unhexlify('0000000431323334')} - for known_input, known_answer in strings.iteritems(): + for known_input, known_answer in strings.items(): assert known_answer == pack_ssh_string(known_input) def test_mpint_known_answers(): # mipint values are from https://www.ietf.org/rfc/rfc4251.txt - mpints = {long(0): '00000000'.decode('hex'), - long(0x9a378f9b2e332a7): '0000000809a378f9b2e332a7'.decode('hex'), - long(0x80): '000000020080'.decode('hex'), long(-0x1234): '00000002edcc'.decode('hex'), - long(-0xdeadbeef): '00000005ff21524111'.decode('hex')} - for known_input, known_answer in mpints.iteritems(): + mpints = {int(0): binascii.unhexlify('00000000'), + int(0x9a378f9b2e332a7): binascii.unhexlify('0000000809a378f9b2e332a7'), + int(0x80): binascii.unhexlify('000000020080'), int(-0x1234): binascii.unhexlify('00000002edcc'), + int(-0xdeadbeef): binascii.unhexlify('00000005ff21524111')} + for known_input, known_answer in mpints.items(): assert known_answer == pack_ssh_mpint(known_input) def test_mpints(): - mpints = {long(-1): '00000001ff'.decode('hex'), long(1): '0000000101'.decode('hex'), - long(127): '000000017f'.decode('hex'), long(128): '000000020080'.decode('hex'), - long(-128): '0000000180'.decode('hex'), long(-129): '00000002ff7f'.decode('hex'), - long(255): '0000000200ff'.decode('hex'), long(256): '000000020100'.decode('hex'), - long(-256): '00000002ff00'.decode('hex'), long(-257): '00000002feff'.decode('hex')} - for known_input, known_answer in mpints.iteritems(): + mpints = {int(-1): binascii.unhexlify('00000001ff'), int(1): binascii.unhexlify('0000000101'), + int(127): binascii.unhexlify('000000017f'), int(128): binascii.unhexlify('000000020080'), + int(-128): binascii.unhexlify('0000000180'), int(-129): binascii.unhexlify('00000002ff7f'), + int(255): binascii.unhexlify('0000000200ff'), int(256): binascii.unhexlify('000000020100'), + int(-256): binascii.unhexlify('00000002ff00'), int(-257): binascii.unhexlify('00000002feff')} + for known_input, known_answer in mpints.items(): assert known_answer == pack_ssh_mpint(known_input) def test_hex_characters_length(): digits = {0: 0, 1: 2, 64: 2, 127: 2, 128: 4, 16384: 4, 32767: 4, 32768: 6, -1: 2, - long(-0x1234): 4, long(-0xdeadbeef): 10, -128: 2} - for known_input, known_answer in digits.iteritems(): + int(-0x1234): 4, int(-0xdeadbeef): 10, -128: 2} + for known_input, known_answer in digits.items(): assert known_answer == _hex_characters_length(known_input) def test_uint32(): - uint32s = {0x00: '00000000'.decode('hex'), 0x0a: '0000000a'.decode('hex'), - 0xab: '000000ab'.decode('hex'), 0xabcd: '0000abcd'.decode('hex'), - 0xabcdef: '00abcdef'.decode('hex'), 0xffffffff: 'ffffffff'.decode('hex'), - 0xf0f0f0f0: 'f0f0f0f0'.decode('hex'), 0x0f0f0f0f: '0f0f0f0f'.decode('hex')} + uint32s = {0x00: binascii.unhexlify('00000000'), 0x0a: binascii.unhexlify('0000000a'), + 0xab: binascii.unhexlify('000000ab'), 0xabcd: binascii.unhexlify('0000abcd'), + 0xabcdef: binascii.unhexlify('00abcdef'), 0xffffffff: binascii.unhexlify('ffffffff'), + 0xf0f0f0f0: binascii.unhexlify('f0f0f0f0'), 0x0f0f0f0f: binascii.unhexlify('0f0f0f0f')} - for known_input, known_answer in uint32s.iteritems(): + for known_input, known_answer in uint32s.items(): assert known_answer == pack_ssh_uint32(known_input) def test_uint64(): - uint64s = {0x00: '0000000000000000'.decode('hex'), 0x0a: '000000000000000a'.decode('hex'), - 0xab: '00000000000000ab'.decode('hex'), 0xabcd: '000000000000abcd'.decode('hex'), - 0xabcdef: '0000000000abcdef'.decode('hex'), - 0xffffffff: '00000000ffffffff'.decode('hex'), - 0xf0f0f0f0: '00000000f0f0f0f0'.decode('hex'), - 0x0f0f0f0f: '000000000f0f0f0f'.decode('hex'), - 0xf0f0f0f000000000: 'f0f0f0f000000000'.decode('hex'), - 0x0f0f0f0f00000000: '0f0f0f0f00000000'.decode('hex'), - 0xffffffffffffffff: 'ffffffffffffffff'.decode('hex')} - - for known_input, known_answer in uint64s.iteritems(): + uint64s = {0x00: binascii.unhexlify('0000000000000000'), 0x0a: binascii.unhexlify('000000000000000a'), + 0xab: binascii.unhexlify('00000000000000ab'), 0xabcd: binascii.unhexlify('000000000000abcd'), + 0xabcdef: binascii.unhexlify('0000000000abcdef'), + 0xffffffff: binascii.unhexlify('00000000ffffffff'), + 0xf0f0f0f0: binascii.unhexlify('00000000f0f0f0f0'), + 0x0f0f0f0f: binascii.unhexlify('000000000f0f0f0f'), + 0xf0f0f0f000000000: binascii.unhexlify('f0f0f0f000000000'), + 0x0f0f0f0f00000000: binascii.unhexlify('0f0f0f0f00000000'), + 0xffffffffffffffff: binascii.unhexlify('ffffffffffffffff')} + + for known_input, known_answer in uint64s.items(): assert known_answer == pack_ssh_uint64(known_input) @@ -76,7 +77,7 @@ def test_uint_too_long(): pack_ssh_uint64(0x1FFFFFFFFFFFFFFFF) with pytest.raises(ValueError): - pack_ssh_uint32(long(0x1FFFFFFFF)) + pack_ssh_uint32(int(0x1FFFFFFFF)) with pytest.raises(ValueError): pack_ssh_uint32(int(0x1FFFFFFFF)) diff --git a/tests/ssh/test_ssh_public_key_ed25519.py b/tests/ssh/test_ssh_public_key_ed25519.py new file mode 100644 index 0000000..4a4e5d7 --- /dev/null +++ b/tests/ssh/test_ssh_public_key_ed25519.py @@ -0,0 +1,29 @@ +import pytest + +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey +from tests.ssh.vectors import EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY_A, \ + EXAMPLE_ECDSA_PUBLIC_KEY, \ + EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION + + +def test_valid_key(): + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY) + assert 'Test ED25519 User Key' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint + + +def test_valid_key_no_description(): + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION) + assert '' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint + + +def test_invalid_keys(): + with pytest.raises(TypeError): + ED25519PublicKey(EXAMPLE_ECDSA_PUBLIC_KEY) + + with pytest.raises(ValueError): + ED25519PublicKey('bogus') + diff --git a/tests/ssh/test_ssh_public_key_factory.py b/tests/ssh/test_ssh_public_key_factory.py index 3485bbb..8175ef8 100644 --- a/tests/ssh/test_ssh_public_key_factory.py +++ b/tests/ssh/test_ssh_public_key_factory.py @@ -2,7 +2,8 @@ from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY, \ - EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E + EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E, \ + EXAMPLE_ED25519_PUBLIC_KEY_A def test_valid_rsa(): @@ -13,9 +14,11 @@ def test_valid_rsa(): assert 'RSA 57:3d:48:4c:65:90:30:8e:39:ba:d8:fa:d0:20:2e:6c' == pub_key.fingerprint -def test_unsupported_ed_25519(): - with pytest.raises(TypeError): - get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY) +def test_valid_ed25519(): + pub_key = get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY) + assert 'Test ED25519 User Key' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint def test_invalid_key(): diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py index bb3c345..4479281 100644 --- a/tests/ssh/vectors.py +++ b/tests/ssh/vectors.py @@ -1,25 +1,30 @@ import base64 # These are for the test CA Private Key. Please don't use it for anything other than unit tests. Just don't. -RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n' -RSA_CA_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,C534195782F2DD74B6218FFF4D3F7576\n\n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\n-----END RSA PRIVATE KEY-----\n' -RSA_CA_PRIVATE_KEY_PASSWORD = 'MdPaveLFOys0hMjL6AQfCdTa7VmzyLkpytuKWAe2RH9zU5KmkOI1P2gjFulGiWg' +RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED = b'-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n' +RSA_CA_PRIVATE_KEY = b'-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,C534195782F2DD74B6218FFF4D3F7576\n\n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\n-----END RSA PRIVATE KEY-----\n' +RSA_CA_PRIVATE_KEY_PASSWORD = b'MdPaveLFOys0hMjL6AQfCdTa7VmzyLkpytuKWAe2RH9zU5KmkOI1P2gjFulGiWg' RSA_CA_SSH_PUBLIC_KEY = base64.b64decode( 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDiNCZ8XiUnbVF7R7u8dzFVS3QzdohQq/AfMQkhrHZiflXSNrARdMXO5efx2VcgE4AgU7fXU+3husxQp0ITyVFAfroVLvRHisLtz6sPZlp018v7Gv0fvsrMBY1NUsscLPogW4hvSBtQSKl2Pnjrl2n/vJBBYvO+241ODa5dFPyi9qqQicIRYd77uVwUNuoof03GVUWj8jkwVajg9DDXlV7NCZFXP4HeJix1oBb3CK0zZpisEdBzUgz6WGy8aJftw232mPQhP7FHrSwOjsTuBBd/D0Cj2PlHdv8sqXOLgvxk9VXxHOAC2VQmWAvArrd4WG+Xn98XWm+1uIpFyMlNr7T0f59Sbv+Z11RV9WNBebtEoWZM/EiO/wSxdouEiDImYTU4KlY/Jq9i6cEhwdOJuvHfRrD5w9ZkEMZXH8e6W3zgI15p01d5kJp+E51tSsSh9J+zSRNY70ct9Z9wq7N2jCenLquvoMWyuHIP1+PENej56bmMzok7Q+YRLruES+HIPOjIuew5nITcG9QrL8RSa7niQeuSeHnyA06prqaQ0FY5GTsx9lEc0cxhNms3XED4HFeWkEImboqnmecRvaBMAf9pDVbt4bDOPboCfTgRh7ZQ6HpguGJEJCXtHoxSVXPLijsxpl34xnFyottQKuYxrxLA0W9w3LixHHjn10aVP+fTqQ==') -EXAMPLE_RSA_PUBLIC_KEY = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw== Test RSA User Key' -EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw==' +EXAMPLE_RSA_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw== Test RSA User Key' +EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw==' EXAMPLE_RSA_PUBLIC_KEY_N = 863765597390437179936880505555316900679139439563534171378010191103037256891865752287401357532386053704846357061277355058425215781749953909539037751959779297844615696266967812249814741811813214887928783792221522790304297884601112319360723158242982362152538534517792264464776081835168055835189781482682493136923038734624107701903646705547410300883750998647287587929185720484066436341331195925984075699168429357216427056893851459700813345902685631135237388578414270244970778240740795928438846416944644092277106238587968690484934416451408183365968935178112856844073906509362367924503252063127972460845602238192159061426436817335330921795349155206805425009150162732786426778585802637172182106209312890241009071807202705546749148063984549775286112624905112511951652674845607083374421597899302609662133400077385016285752279857194248901093535059948349008431744427219342487822381449072585539524484989393533507171281966556907880034930485429159724230925010429126325954683461210886022933944242055541038221737797142318602403013636630990332566384663121261896469133144198125969293719738317514483451902518238751088148376136388153900264080474983146540911501501723872658100092602399884083442609325830811845748736526156589543373619454484831196371149151 EXAMPLE_RSA_PUBLIC_KEY_E = 65537 -EXAMPLE_ED25519_PUBLIC_KEY = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' +EXAMPLE_ED25519_PUBLIC_KEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' -EXAMPLE_ECDSA_PUBLIC_KEY = u'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' +EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp' -EXAMPLE_RSA_PUBLIC_KEY_2048 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33' -EXAMPLE_RSA_PUBLIC_KEY_1024 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCxc0Yw+5HMM2h1iPKGp4uoh9rNNbmfDgTmv9nCT9QRo4QgKzX/wrA4JSzpSSZLC2qIHADVESeWsOEi9Ohko7lgPGD+vAHFfNv/TNK8EFTzAbSoM/7eriO3wR5Y668Z0yEWhoCVnUWAHBWCBkQgHTxRYV/feXTirbI4ugp0v35d6w==' -EXAMPLE_RSA_PUBLIC_KEY_SMALLPRIME = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQI3LzCywCXa4DNeS4gzyDWKeLJqauP6CSLXWjxVLi2iIzroKx8YM1nJVHXfLFseDgfAZGaJhLBXxyDr5vh47U086SP0TFf6JowWofNOerTzotMRNhx9TH/vVu/IGCCPs9P0RVr2g04XcagyA7hjTc26e5jvIkDtczKAMeo/jHoSm2Ot9HA2b+KjZkiZGBIflzJ1uadkNnh+IBlMymJH7GAPbRF9pvbc/XlRT3czwG+AD6d+CHij5pd+91Y8pwpV1QiTxJo1moiea7d7mOmX5RQc6b+YC9CtThN92mzrWsq6wDLCawJZODHL2/hRnjmX6PlvuazfMZv5KA2dr7mh8Xnl' -EXAMPLE_RSA_PUBLIC_KEY_E3 = u'ssh-rsa AAAAB3NzaC1yc2EAAAABAwAAAQEAvQ+65kAMnkq7yhktZpgR2NLmI3j2qK22R8i+xw9ki2ETorkKXWZzQxwnSmQeX1oClXbM2IGQHUJgTqJS0vnEaaML/BlyqLeEB4tRGijm++DxBby0KcQqpRz6mAgK2pFGpsHI/NZvXSXiu1aSy8SZ6NPdpQtq+dEQ1WX4v9l+BjPL5KbQEiVLi8zC3bKwtTJmJz3idrzS1LVdxEN2F/l1Wnmwfzennv8ocG/SZpV6gAU31K19i/eH1P0cvuJYx0cC2+wzZzOC33k9KTL4h/cGtE3qiAPwOcSxKfN5o8juPkAQ63kAyGgQmUlSxd9oh/hTJT3kSmXeqGKvNI/oi1B99w==' +EXAMPLE_ED25519_PUBLIC_KEY_A = base64.b64decode( + 'bv5wBtPgQVI+zzuhLiQNhK1DQovTHAdapVJnZtgcgCk=') + +EXAMPLE_ECDSA_PUBLIC_KEY = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' + +EXAMPLE_RSA_PUBLIC_KEY_2048 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33' +EXAMPLE_RSA_PUBLIC_KEY_1024 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCxc0Yw+5HMM2h1iPKGp4uoh9rNNbmfDgTmv9nCT9QRo4QgKzX/wrA4JSzpSSZLC2qIHADVESeWsOEi9Ohko7lgPGD+vAHFfNv/TNK8EFTzAbSoM/7eriO3wR5Y668Z0yEWhoCVnUWAHBWCBkQgHTxRYV/feXTirbI4ugp0v35d6w==' +EXAMPLE_RSA_PUBLIC_KEY_SMALLPRIME = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQI3LzCywCXa4DNeS4gzyDWKeLJqauP6CSLXWjxVLi2iIzroKx8YM1nJVHXfLFseDgfAZGaJhLBXxyDr5vh47U086SP0TFf6JowWofNOerTzotMRNhx9TH/vVu/IGCCPs9P0RVr2g04XcagyA7hjTc26e5jvIkDtczKAMeo/jHoSm2Ot9HA2b+KjZkiZGBIflzJ1uadkNnh+IBlMymJH7GAPbRF9pvbc/XlRT3czwG+AD6d+CHij5pd+91Y8pwpV1QiTxJo1moiea7d7mOmX5RQc6b+YC9CtThN92mzrWsq6wDLCawJZODHL2/hRnjmX6PlvuazfMZv5KA2dr7mh8Xnl' +EXAMPLE_RSA_PUBLIC_KEY_E3 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABAwAAAQEAvQ+65kAMnkq7yhktZpgR2NLmI3j2qK22R8i+xw9ki2ETorkKXWZzQxwnSmQeX1oClXbM2IGQHUJgTqJS0vnEaaML/BlyqLeEB4tRGijm++DxBby0KcQqpRz6mAgK2pFGpsHI/NZvXSXiu1aSy8SZ6NPdpQtq+dEQ1WX4v9l+BjPL5KbQEiVLi8zC3bKwtTJmJz3idrzS1LVdxEN2F/l1Wnmwfzennv8ocG/SZpV6gAU31K19i/eH1P0cvuJYx0cC2+wzZzOC33k9KTL4h/cGtE3qiAPwOcSxKfN5o8juPkAQ63kAyGgQmUlSxd9oh/hTJT3kSmXeqGKvNI/oi1B99w==' # ssh-keygen -s test-rsa-ca -I user@test -n username -V -5m:+5m test-rsa-user RSA_USER_CERT = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' @@ -29,7 +34,7 @@ RSA_USER_CERT_DEFAULTS = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' RSA_USER_CERT_DEFAULTS_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub' # ssh-keygen -s test-rsa-ca -I "ssh-keygen -s test-rsa-ca -I '' test-rsa-user-all-defaults.pub" test-rsa-user-all-defaults.pub -RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT = 'ssh-rsa-cert-v01@openssh.com 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 Certificate type:1 principals:[] with the id:[ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub]' +RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT = 'ssh-rsa-cert-v01@openssh.com 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 Certificate type[1] principals[] with the id[ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub]' RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub' # ssh-keygen -s test-rsa-ca -n user1,user2,other_user1,other_user2 -I "ssh-keygen -s test-rsa-ca -n user1,user2,other_user1,other_user2 -I '' test-rsa-user-many-principals.pub" test-rsa-user-many-principals.pub RSA_USER_CERT_MANY_PRINCIPALS = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' @@ -45,4 +50,8 @@ 'AAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') SSH_CERT_CUSTOM_EXTENSIONS = base64.b64decode( - 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') \ No newline at end of file + 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') + +# ssh-keygen -s test-rsa-ca -I "ssh-keygen -s test-rsa-ca -I '' test-ed25519-user-all-defaults.pub" test-ed25519-user-all-defaults.pub +ED25519_USER_CERT_DEFAULTS = 'ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINa9ZmyR9YBRNfC464IJE2AlDa0xU02tVbY37AlRr79/AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIApAAAAAAAAAAAAAAABAAAAQnNzaC1rZXlnZW4gLXMgdGVzdC1yc2EtY2EgLUkgJycgdGVzdC1lZDI1NTE5LXVzZXItYWxsLWRlZmF1bHRzLnB1YgAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAOI0JnxeJSdtUXtHu7x3MVVLdDN2iFCr8B8xCSGsdmJ+VdI2sBF0xc7l5/HZVyATgCBTt9dT7eG6zFCnQhPJUUB+uhUu9EeKwu3Pqw9mWnTXy/sa/R++yswFjU1Syxws+iBbiG9IG1BIqXY+eOuXaf+8kEFi877bjU4Nrl0U/KL2qpCJwhFh3vu5XBQ26ih/TcZVRaPyOTBVqOD0MNeVXs0JkVc/gd4mLHWgFvcIrTNmmKwR0HNSDPpYbLxol+3DbfaY9CE/sUetLA6OxO4EF38PQKPY+Ud2/yypc4uC/GT1VfEc4ALZVCZYC8Cut3hYb5ef3xdab7W4ikXIyU2vtPR/n1Ju/5nXVFX1Y0F5u0ShZkz8SI7/BLF2i4SIMiZhNTgqVj8mr2LpwSHB04m68d9GsPnD1mQQxlcfx7pbfOAjXmnTV3mQmn4TnW1KxKH0n7NJE1jvRy31n3Crs3aMJ6cuq6+gxbK4cg/X48Q16PnpuYzOiTtD5hEuu4RL4cg86Mi57DmchNwb1CsvxFJrueJB65J4efIDTqmuppDQVjkZOzH2URzRzGE2azdcQPgcV5aQQiZuiqeZ5xG9oEwB/2kNVu3hsM49ugJ9OBGHtlDoemC4YkQkJe0ejFJVc8uKOzGmXfjGcXKi21Aq5jGvEsDRb3DcuLEceOfXRpU/59OpAAACDwAAAAdzc2gtcnNhAAACACnir9y/PhoKvLNvs4fMkS24mrHyGxc24nTxaKJO9OBGr4PY3Glg90hKaZvC/cLN0wSAeIYzigp/PGm1PNIM342NqxMkIx7yUKrEbcIQaommn8kfThMIcLJo9QOpDoMAxOsfAtZrtWQKMMYM6s8hZKrb3OT8k0le/x4S2GttHMb9z006nZgyTUI4cniq+ZxwcL4l5/wzaXamQUsod/JRysEBZdLF88A50iCiSC+4NQUqquPBs76UmTpU+Lv3OEvVBwMil7Hxkzv8kqit2Xal0Ou9n/+CC1G9l/dpTO1TgaNtNERQT3rQOhkjqwmgN3wbh1sRkc1+zSenWsNrHkFBnVTJPxLwXbFeAvstDWDxTvNCpygsTfz/ejUnKfqZ1rAWfNRhzrSIz1D1+/GQbOdtM3xq3r6CVjVxE0KTLsdR1hyOd5SmszubE1UAkKWF7NRyHcgma/9hkOXc6a/4ylBcOj0yUFnjVq7Jb6C33ba0Ra6LnopZWUS7lr02dt7aYG/Qhd8OtJx7R+XiRYRnfsuJH+L18UxM34xqj9qlMPA5p1nUB2ZnklKyueLhrp0/thuWsdKCv4w66A8rOthbDtLip6TYtKwDDMBupq5ROoRHYXb6nYthHJYCX1QuDIzmoBjTlkWa7aVohggQWezYhGGo0owulURxkFNZBNi1Lc/aRuDs Test ED25519 User Key' +ED25519_USER_CERT_DEFAULTS_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-ed25519-user-all-defaults.pub'