diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa9bb50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.coverage +*.pyc +*.cache/ +.idea/ +bless.egg-info/ +htmlcov/ +libs/ +publish/ +venv/ +aws_lambda_libs/ +lambda_configs/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..82618eb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: false + +language: python + +addons: + +matrix: + include: + - python: "2.7" + +install: + - make develop + +before_script: + +script: + - make test + +notifications: + email: + russelll@netflix.com diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..e946dd3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +- Russell Lewis diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1280afa --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Netflix, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38c8693 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +test: lint + @echo "--> Running Python tests" + py.test tests || exit 1 + @echo "" + +develop: + @echo "--> Installing dependencies" + pip install -r requirements.txt + pip install "file://`pwd`#egg=bless[tests]" + @echo "" + +dev-docs: + # todo the docs, so typical, right? + +clean: + @echo "--> Cleaning pyc files" + find . -name "*.pyc" -delete + rm -rf ./publish ./htmlcov + @echo "" + +lint: + @echo "--> Linting Python files" + PYFLAKES_NODOCTEST=1 flake8 bless + @echo "" + +coverage: + coverage run --branch --source=bless -m py.test tests + coverage html + +publish: + mkdir -p ./publish/bless_lambda + cp -r ./bless ./publish/bless_lambda/ + mv ./publish/bless_lambda/bless/aws_lambda/* ./publish/bless_lambda/ + cp -r ./aws_lambda_libs/ ./publish/bless_lambda/ + cp -r ./lambda_configs/ ./publish/bless_lambda/ + rm ./publish/bless_lambda/place_cfg_and_pem_here ./publish/bless_lambda/place_compiled_dependencies_here + cd ./publish/bless_lambda && zip -r ../bless_lambda.zip . + +.PHONY: develop dev-docs clean test lint coverage publish \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e51d447 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +![alt text](bless_logo.png "BLESS") +# BLESS - Bastion's Lambda Ephemeral Ssh Service +BLESS is an SSH Certificate Authority that runs as a 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, +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. + +BLESS should be run as an AWS Lambda in an isolated AWS account. Because BLESS needs access to a +private key which is trusted by your hosts, an isolated AWS account helps restrict who can access +that private key, or modify the BLESS code you are running. + +AWS Lambda functions can use an AWS IAM Policy to limit which IAM Roles can invoke the Lambda +Function. If properly configured, you can restrict which IAM Roles can request SSH Certificates. +For example, your SSH Bastion (aka SSH Jump Host) can run with the only IAM Role with access to +invoke a BLESS Lambda Function configured with the SSH CA key trusted by the instances accessible +to that SSH Bastion. + +## Getting Started +These instructions are to get BLESS up and running in your local development environment. +### Installation Instructions +Clone the repo: + + $ git clone git@github.com:Netflix/bless.git + +Cd to the bless repo: + + $ cd bless + +Create a virtualenv if you haven't already: + + $ virtualenv venv + +Activate the venv: + + $ source venv/bin/activate + +Install package and test dependencies: + + (venv) $ make develop + +Run the tests: + + (venv) $ make test + + +## Deployment +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. + +### 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 +compile and include your dependencies before you can publish a working AWS Lambda. + +- 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 +- Install BLESS' dependencies: +``` +$ sudo yum install gcc libffi-devel openssl-devel +$ virtualenv venv +$ source venv/bin/activate +(venv) $ pip install -e . +``` +- From that instance, copy off the contents of: + - venv/lib/python2.7/site-packages/* + - venv/lib64/python2.7/site-packages/* + +- put those files in: ./aws-linux-libs/ + +### Protecting the CA Private Key +- Generate a password protected RSA Private Key: +``` +$ ssh-keygen -t rsa -b 4096 -f bless-ca- -C "SSH CA Key" +``` +- 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: +``` +import boto3 +import base64 +import os + + +def lambda_handler(event, context): + region = os.environ['AWS_REGION'] + client = boto3.client('kms', region_name=region) + response = client.encrypt( + KeyId='alias/your_kms_key', + Plaintext='Do not forget to delete the real plain text when done' + ) + + ciphertext = response['CiphertextBlob'] + return base64.b64encode(ciphertext) +``` + +- Manage your Private Keys .pem files and passwords outside of this repo. +- Update your bless_deploy.cfg with your Private Key's filename and encrypted passwords. +- Provide your desired ./lambda_configs/ca_key_name.pem prior to Publishing a new Lambda .zip + +### 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. + +### Publish Lambda .zip +- Provide your desired ./lambda_configs/ca_key_name.pem prior to Publishing +- Provide your desired [BLESS Config File](bless/config/bless_deploy_example.cfg) at +./lambda_configs/bless_deploy.cfg prior to Publishing +- Provide the [compiled dependencies](#compiling-bless-lambda-dependencies) at ./aws-linux-libs +- run: +``` +(venv) $ make publish +``` + +- deploy ./publish/bless_lambda.zip to AWS via the AWS Console, +[AWS SDK](http://boto3.readthedocs.io/en/latest/reference/services/lambda.html), or +[S3](https://aws.amazon.com/blogs/compute/new-deployment-options-for-aws-lambda/) +- remember to deploy it to all regions. + + +### Lambda Requirements +You should deploy this function into its own AWS account to limit who has access to modify the +code, configs, or IAM Policies. An isolated account also limits who has access to the KMS keys +used to protect the SSH CA Key. + +The BLESS Lambda function should run as its own IAM Role and will need access to an AWS KMS Key in +each region where the function is deployed. The BLESS IAMRole will also need permissions to obtain +random from kms (kms:GenerateRandom) and permissions for logging to CloudWatch Logs +(logs:CreateLogGroup,logs:CreateLogStream,logs:PutLogEvents). + +## 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). + + (venv) $ ./bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username bastion_source_ip bastion_command + + +## Verifying Certificates +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: + + TrustedUserCAKeys /etc/ssh/cas.pub + +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 + ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… #id_rsa.pub of an offline SSH CA 2 + +To simplify SSH CA Key rotation you should provision multiple CA Keys, and leave them offline until +you are ready to rotate them. + +Additional information about the TrustedUserCAKeys file is [here](https://www.freebsd.org/cgi/man.cgi?sshd_config(5)) + +## Project resources +- Source code +- Issue tracker diff --git a/bless/__about__.py b/bless/__about__.py new file mode 100644 index 0000000..286b0f8 --- /dev/null +++ b/bless/__about__.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import, division, print_function + +__all__ = [ + "__title__", "__summary__", "__uri__", "__version__", "__author__", + "__email__", "__license__", "__copyright__", +] + +__title__ = "BLESS" +__summary__ = ( + "BLESS is an SSH Certificate Authority that runs as a AWS Lambda function and can be used to " + "sign ssh public keys.") +__uri__ = "https://github.com/Netflix/bless" + +__version__ = "0.1.0dev" + +__author__ = "The BLESS developers" +__email__ = "security@netflix.com" + +__license__ = "Apache License, Version 2.0" +__copyright__ = "Copyright 2016 {0}".format(__author__) diff --git a/bless/__init__.py b/bless/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/aws_lambda/__init__.py b/bless/aws_lambda/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py new file mode 100644 index 0000000..89e8443 --- /dev/null +++ b/bless/aws_lambda/bless_lambda.py @@ -0,0 +1,117 @@ +""" +.. module: bless.aws_lambda.bless_lambda + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import logging +import time + +import boto3 +import os +from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_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')): + """ + 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. + """ + # 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_window_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_WINDOW_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_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + password_ciphertext_b64 = config.getpassword() + + # read the private key .pem + with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f: + ca_private_key = f.read() + + # decrypt ca private key password + if ca_private_key_password is None: + kms_client = boto3.client('kms', region_name=region) + ca_password = kms_client.decrypt( + CiphertextBlob=base64.b64decode(password_ciphertext_b64)) + ca_private_key_password = ca_password['Plaintext'] + + # 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) + + # Process cert request + schema = BlessSchema(strict=True) + request = schema.load(event).data + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + valid_before = current_time + certificate_validity_window_seconds + valid_after = current_time - certificate_validity_window_seconds + + # 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) + cert_builder.add_valid_principal(request.remote_username) + 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[{}] 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_address(request.bastion_ip) + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + logger.info( + 'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and ' + 'valid_from[{}])'.format( + request.bastion_ip, request.remote_username, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return cert diff --git a/bless/config/__init__.py b/bless/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py new file mode 100644 index 0000000..1208606 --- /dev/null +++ b/bless/config/bless_config.py @@ -0,0 +1,59 @@ +""" +.. module: bless.config.bless_config + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import ConfigParser + +BLESS_OPTIONS_SECTION = 'Bless Options' +CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' +CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 + +ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' +ENTROPY_MINIMUM_BITS_DEFAULT = 2048 + +RANDOM_SEED_BYTES_OPTION = 'random_seed_bytes' +RANDOM_SEED_BYTES_DEFAULT = 256 + +LOGGING_LEVEL_OPTION = 'logging_level' +LOGGING_LEVEL_DEFAULT = 'INFO' + +BLESS_CA_SECTION = 'Bless CA' +CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' +KMS_KEY_ID_OPTION = 'kms_key_id' + +REGION_PASSWORD_OPTION_SUFFIX = '_password' + + +class BlessConfig(ConfigParser.RawConfigParser): + def __init__(self, aws_region, config_file): + """ + Parses the BLESS config file, and provides some reasonable default values if they are + absent from the config file. + + The [Bless Options] section is entirely optional, and has defaults. + + The [Bless CA] section is required. + :param aws_region: The AWS Region BLESS is deployed to. + :param config_file: Path to the connfig file. + """ + self.aws_region = aws_region + defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, + ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT, + RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT, + LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT} + ConfigParser.RawConfigParser.__init__(self, defaults=defaults) + self.read(config_file) + + if not self.has_section(BLESS_OPTIONS_SECTION): + self.add_section(BLESS_OPTIONS_SECTION) + + if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): + raise ValueError("No Region Specific Password Provided.") + + def getpassword(self): + """ + Returns the correct encrypted password based off of the aws_region. + :return: A Base64 encoded KMS CiphertextBlob. + """ + return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg new file mode 100644 index 0000000..17d1397 --- /dev/null +++ b/bless/config/bless_deploy_example.cfg @@ -0,0 +1,21 @@ +# This section and its options are optional +[Bless Options] +# Number of seconds +/- the issued time for the certificate to be valid +certificate_validity_window_seconds = 120 +# Minimum number of bits in the system entropy pool before requiring an additional seeding step +entropy_minimum_bits = 2048 +# Number of bytes of random to fetch from KMS to seed /dev/urandom +random_seed_bytes = 256 +# Set the logging level +logging_level = INFO + +# These values are all required to be modified for deployment +[Bless CA] +# AWS KMS key alias used to encrypt your private key password +kms_key_id = +# You must set an encrypted private key password for each AWS Region you deploy into +# for each aws region specify a config option like '{}_password'.format(aws_region) +us-east-1_password = +us-west-2_password = +# Specify the file name of your SSH CA's Private Key in PEM format. +ca_private_key_file = \ No newline at end of file diff --git a/bless/request/__init__.py b/bless/request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py new file mode 100644 index 0000000..d3db410 --- /dev/null +++ b/bless/request/bless_request.py @@ -0,0 +1,64 @@ +""" +.. module: bless.request.bless_request + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import ipaddress +import re +from marshmallow import Schema, fields, post_load, ValidationError + +# man 8 useradd +USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') + + +def validate_ip(ip): + try: + ipaddress.ip_address(ip) + 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') + + +class BlessSchema(Schema): + bastion_ip = fields.Str(validate=validate_ip) + bastion_user = fields.Str(validate=validate_user) + bastion_user_ip = fields.Str(validate=validate_ip) + command = fields.Str() + public_key_to_sign = fields.Str() + remote_username = fields.Str(validate=validate_user) + + @post_load + def make_bless_request(self, data): + return BlessRequest(**data) + + +class BlessRequest: + def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_key_to_sign, + remote_username): + """ + A BlessRequest must have the following key value pairs to be valid. + :param bastion_ip: The source IP 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_username: The username on the remote server that will be used in the ssh + request. This is enforced in the issued certificate. + """ + self.bastion_ip = bastion_ip + 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_username = remote_username + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/bless/ssh/__init__.py b/bless/ssh/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/ssh/certificate_authorities/__init__.py b/bless/ssh/certificate_authorities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/ssh/certificate_authorities/rsa_certificate_authority.py b/bless/ssh/certificate_authorities/rsa_certificate_authority.py new file mode 100644 index 0000000..385c77f --- /dev/null +++ b/bless/ssh/certificate_authorities/rsa_certificate_authority.py @@ -0,0 +1,61 @@ +""" +.. module: bless.ssh.certificate_authorities.rsa_certificate_authority + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.certificate_authorities.ssh_certificate_authority import \ + SSHCertificateAuthority +from bless.ssh.protocol.ssh_protocol import pack_ssh_mpint, pack_ssh_string +from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.serialization import load_pem_private_key + + +class RSACertificateAuthority(SSHCertificateAuthority): + def __init__(self, pem_private_key, private_key_password=None): + """ + RSA Certificate Authority used to sign certificates. + :param pem_private_key: PEM formatted RSA Private Key. It should be encrypted with a + password, but that is not required. + :param private_key_password: Password to decrypt the PEM RSA Private Key, if it is + encrypted. Which it should be. + """ + super(SSHCertificateAuthority, self).__init__() + self.public_key_type = SSHPublicKeyType.RSA + + self.private_key = load_pem_private_key(pem_private_key, + 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 + self.n = ca_pub_numbers.n + + def get_signature_key(self): + """ + Get the SSH Public Key associated with this CA. + Packed per RFC4253 section 6.6. + :return: SSH Public Key. + """ + key = pack_ssh_string(self.public_key_type) + key += pack_ssh_mpint(self.e) + key += pack_ssh_mpint(self.n) + return key + + def sign(self, body): + """ + Sign the certificate body with the RSA private key. Signatures are computed and + encoded per RFC4253 section 6.6 + :param body: All other fields of the SSH Certificate, from the initial string to the + signature key. + :return: SSH RSA Signature. + """ + self.signer.update(body) + signature = self.signer.finalize() + + return self._serialize_signature(signature) diff --git a/bless/ssh/certificate_authorities/ssh_certificate_authority.py b/bless/ssh/certificate_authorities/ssh_certificate_authority.py new file mode 100644 index 0000000..2531dea --- /dev/null +++ b/bless/ssh/certificate_authorities/ssh_certificate_authority.py @@ -0,0 +1,43 @@ +""" +.. module: bless.ssh.certificate_authorities.ssh_certificate_authority + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.protocol.ssh_protocol import pack_ssh_string + + +class SSHCertificateAuthorityPrivateKeyType(object): + RSA = '-----BEGIN RSA PRIVATE KEY-----\n' + # todo support other CA Private Key Types + + +class SSHCertificateAuthority(object): + def __init__(self): + self.public_key_type = None + + # todo real abstract classes + def sign(self, body): + """ + Sign the certificate body with the CA private key. Signatures are computed and + encoded per RFC4253 section 6.6 + :param body: All other fields of the SSH Certificate, from the initial string to the + signature key. + :return: SSH Signature. + """ + raise NotImplementedError("Child classes should override this") + + # todo real abstract classes + def get_signature_key(self): + """ + Get the SSH Public Key associated with this CA. + Packed per RFC4253 section 6.6 + :return: SSH Certificate formatted Public Key. + """ + raise NotImplementedError("Child classes should override this") + + def _serialize_signature(self, signature): + # pack signature block + sig_inner = pack_ssh_string(self.public_key_type) + sig_inner += pack_ssh_string(signature) + + return pack_ssh_string(sig_inner) diff --git a/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py new file mode 100644 index 0000000..f080767 --- /dev/null +++ b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py @@ -0,0 +1,23 @@ +""" +.. module: bless.ssh.certificate_authorities.ssh_certificate_authority_factory + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.certificate_authorities.rsa_certificate_authority import \ + RSACertificateAuthority +from bless.ssh.certificate_authorities.ssh_certificate_authority import \ + SSHCertificateAuthorityPrivateKeyType + + +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). + 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. + :return: An SSHCertificateAuthority instance. + """ + if private_key.startswith(SSHCertificateAuthorityPrivateKeyType.RSA): + return RSACertificateAuthority(private_key, password) + else: + raise TypeError("Unsupported CA Private Key Type") diff --git a/bless/ssh/certificates/__init__.py b/bless/ssh/certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/ssh/certificates/rsa_certificate_builder.py b/bless/ssh/certificates/rsa_certificate_builder.py new file mode 100644 index 0000000..193a3bc --- /dev/null +++ b/bless/ssh/certificates/rsa_certificate_builder.py @@ -0,0 +1,40 @@ +""" +.. module: bless.ssh.certificates.rsa_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_mpint + + +class RSACertificateBuilder(SSHCertificateBuilder): + def __init__(self, ca, cert_type, ssh_public_key_rsa): + """ + Produces an SSH certificate for RSA 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_rsa: The RSAPublicKey to issue a certificate for. + """ + super(RSACertificateBuilder, self).__init__(ca, cert_type) + self.cert_key_type = SSHCertifiedKeyType.RSA + self.ssh_public_key = ssh_public_key_rsa + self.public_key_comment = ssh_public_key_rsa.key_comment + self.e = ssh_public_key_rsa.e + self.n = ssh_public_key_rsa.n + + def _serialize_ssh_public_key(self): + """ + Serialize the Public Key into the RSA exponent and public modulus stored as SSH mpints. + http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :return: The bytes that belong in the SSH Certificate between the nonce and the + certificate serial number. + """ + public_key = pack_ssh_mpint(self.e) + public_key += pack_ssh_mpint(self.n) + return public_key diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py new file mode 100644 index 0000000..67b0adf --- /dev/null +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -0,0 +1,305 @@ +""" +.. module: bless.ssh.certificates.ssh_certificate_builder + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 + +import os +from bless.ssh.protocol.ssh_protocol import pack_ssh_string, pack_ssh_uint64, pack_ssh_uint32 + + +class SSHCertificateType(object): + USER = 1 + HOST = 2 + + +class SSHCertifiedKeyType(object): + RSA = 'ssh-rsa-cert-v01@openssh.com' + ED25519 = 'ssh-ed25519-cert-v01@openssh.com' + # todo support more key types: + # 'ecdsa-sha2-nistp256-cert-v01@openssh.com' + # 'ecdsa-sha2-nistp384-cert-v01@openssh.com' + # 'ecdsa-sha2-nistp521-cert-v01@openssh.com' + + +class SSHCertificateBuilder(object): + def __init__(self, ca, cert_type): + """ + An abstract base class used to produce an SSH Certificate for various public key types. + :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 + """ + self.ca = ca # required + self.nonce = None # optional, has default = os.urandom(32) + self.public_key_comment = None + self.serial = None # can be set, has default = 0 + self.cert_type = None # required: User = 1, Host = 2 + self.key_id = None # optional, default = '' + self.valid_principals = list() # optional, default = '' + self.valid_after = None # optional, default = 0 + self.valid_before = None # optional, default = 2^64-1 + self.critical_option_force_command = None # optional, default = '' + self.critical_option_source_address = None # optional, default = '' + self.extensions = None # optional, default = '' + self.reserved = '' # should always be this value + self.signature = None + self.signed_cert = None + self.public_key_comment = None + self.cert_type = cert_type + + # todo real abstract classes + def _serialize_ssh_public_key(self): + """ + Serialize the Public Key per the spec: + http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :return: The bytes that belong in the SSH Certificate between the nonce and the + certificate serial number. + """ + raise NotImplementedError("Child classes should override this") + + def set_nonce(self, nonce=None): + """ + Sets the nonce to be included as a part of the certificate body. + :param nonce: If no nonce is specified, this will fetch 32 Bytes from os.urandom. + """ + if nonce is None: + nonce = os.urandom(32) + self.nonce = nonce + + def set_serial(self, serial=0): + """ + Sets an optional serial number of the SSH Certificate. + :param serial: A uint64 serial number. + """ + self.serial = serial + + def set_key_id(self, key_id=''): + """ + Sets the key id of a certificate, which is just a string that ends up getting singed by + the CA. This key id is super useful because it gets logged by sshd when the certificate + is used to successfully authenticate users. Depending on your environment, the logging of + this string will eventually be truncated at ~325 characters. + :param key_id: String to include in the certificate, to be logged when the certificate + is used. + """ + self.key_id = key_id + + def add_valid_principal(self, valid_principal): + """ + Individually add one valid principal to the certificate. You can add many principals to an + SSH Certificate. + + For User SSH Certificates, a valid principal defines which remote user account(s) the + certificate is valid for. + + For Host SSH Certificates, a valid principal defines which hostname(s) the certificate is + valid for. + + You want to set at least one valid principal. Not doing means the certificate is valid + for any user/hostname. + See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :param valid_principal: String with the username or hostname. + """ + if valid_principal: + if valid_principal not in self.valid_principals: + self.valid_principals.append(valid_principal) + else: + raise ValueError("Principal already added.") + else: + raise ValueError("Provide a non-null string") + + def set_valid_after(self, after=0): + """ + Sets the SSH Certificate validity start time. Not setting a value will result in an SSH + Certificate that is valid since time 0. + :param after: Integer of the desired Unix epoch time. + """ + self.valid_after = after + + def set_valid_before(self, before=18446744073709551615): + """ + Sets the SSH Certificate validity end time. Not setting a value will result in an SSH + Certificate that never expires. Probably not what you want to do. + :param before: Integer of the desired Unix epoch time + """ + self.valid_before = before + + def set_critical_option_force_command(self, command): + """ + Sets a command that will be executed whenever this SSH Certificate is used for + authentication. This will replace any command specified by the ssh command. + :param command: String of the program (and arguments) to run on the remote host. + """ + if command: + self.critical_option_force_command = command + else: + raise ValueError("Provide a non-null string") + + def set_critical_option_source_address(self, address): + """ + Sets which IP address(es) this certificate can be used from for authentication. Addresses + should be comma-separated and can be individual IPs or CIDR format (nn.nn.nn.nn/nn or + hhhh::hhhh/nn). + + Not setting this means the SSH Certificate is valid from any IP. Probably not what you + want to do. + :param address: String of one or more comma-separated IPs or CIDRs. + """ + if address: + self.critical_option_source_address = address + else: + raise ValueError("Provide a non-null string") + + def clear_extensions(self): + """ + Removes any previously set SSH Certificate Extensions. + """ + self.extensions = set() + + def set_extensions_to_default(self): + """ + Sets the SSH Certificate Extensions set to the same defaults ssh-keygen would provide. + + SSH Certificate Extensions enable certain SSH features. If they are not present, + sessions authenticated with the certificate cannot use them. + + See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + """ + if self.cert_type is SSHCertificateType.USER: + self.extensions = {'permit-X11-forwarding', + 'permit-agent-forwarding', + 'permit-port-forwarding', + 'permit-pty', 'permit-user-rc'} + else: + # SSHCertificateType.HOST has no applicable extensions. + self.clear_extensions() + + def add_extension(self, extension): + """ + Add an individual SSH Certificate Extension to the certificate. + + SSH Certificate Extensions enable certain SSH features. If they are not present, + sessions authenticated with the certificate cannot use them. + + See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :param extension: the extension to include + """ + if self.extensions is None: + self.extensions = set() + + self.extensions.add(extension) + + def get_cert_file(self): + """ + Generate the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + + This will initialize any unset SSH Certificate attributes to sane defaults, verify the + validity range, and sign the certificate. + :return: String with all of the required SSH Certificate contents, that can be written + to a file. + """ + file_contents = ( + "{} {} {}" + ).format(self.cert_key_type, base64.b64encode(self._sign_cert()), + self.public_key_comment) + return file_contents + + def _initialize_unset_attributes(self): + if self.nonce is None: + self.set_nonce() + + if self.serial is None: + self.set_serial() + + if self.valid_after is None: + self.set_valid_after() + + if self.valid_before is None: + self.set_valid_before() + + if self.key_id is None: + self.set_key_id() + + if self.extensions is None: + self.set_extensions_to_default() + + 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) + + def _validate_cert_properties(self): + if self.valid_after >= self.valid_before: + raise ValueError("Impossible validity period") + + def _sign_cert(self): + if self.signed_cert is None: + # build cert body + self._initialize_unset_attributes() + self._validate_cert_properties() + body_bytes = self._serialize_certificate_body() + + # sign the body + sig_bytes = self.ca.sign(body_bytes) + self.signed_cert = body_bytes + sig_bytes + return self.signed_cert + + def _serialize_certificate_body(self): + body = pack_ssh_string(self.cert_key_type) + body += pack_ssh_string(self.nonce) + body += self._serialize_ssh_public_key() + body += pack_ssh_uint64(self.serial) + body += pack_ssh_uint32(self.cert_type) + body += pack_ssh_string(self.key_id) + body += pack_ssh_string(self._serialize_valid_principals()) + body += pack_ssh_uint64(self.valid_after) + body += pack_ssh_uint64(self.valid_before) + body += pack_ssh_string(self._serialize_critical_options()) + body += pack_ssh_string(self._serialize_extensions()) + body += pack_ssh_string('') + body += pack_ssh_string(self.ca.get_signature_key()) + return body + + def _serialize_extensions(self): + # Options must be lexically ordered by "name" if they appear in the + # sequence. Each named option may only appear once in a certificate. + extensions_list = sorted(self.extensions) + + serialized = '' + # Format is a series of {extension name}{empty string} + for extension in extensions_list: + serialized += pack_ssh_string(extension) + serialized += pack_ssh_string('') + + return serialized + + def _serialize_valid_principals(self): + serialized = '' + + for principal in self.valid_principals: + serialized += pack_ssh_string(principal) + + return serialized + + 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 = '' + + if self.critical_option_force_command is not None: + serialized += pack_ssh_string('force-command') + serialized += pack_ssh_string( + pack_ssh_string(self.critical_option_force_command)) + + if self.critical_option_source_address is not None: + serialized += pack_ssh_string('source-address') + serialized += pack_ssh_string( + pack_ssh_string(self.critical_option_source_address)) + + return serialized diff --git a/bless/ssh/certificates/ssh_certificate_builder_factory.py b/bless/ssh/certificates/ssh_certificate_builder_factory.py new file mode 100644 index 0000000..e26d7b7 --- /dev/null +++ b/bless/ssh/certificates/ssh_certificate_builder_factory.py @@ -0,0 +1,27 @@ +""" +.. module: bless.ssh.certificates.ssh_certificate_builder_factory + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.certificates.rsa_certificate_builder \ + import RSACertificateBuilder +from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType +from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key + + +def get_ssh_certificate_builder(ca, cert_type, public_key_to_sign): + """ + Returns the proper SSHCertificateBuilder instance for the type of public key to be signed. + :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? + :param public_key_to_sign: The SSHPublicKey to issue a certificate for. + :return: An SSHCertificateBuilder instance. + """ + # Determine the type of public key we have, to decide the right cert type + ssh_public_key = get_ssh_public_key(public_key_to_sign) + + if ssh_public_key.type is SSHPublicKeyType.RSA: + return RSACertificateBuilder(ca, cert_type, ssh_public_key) + else: + raise TypeError("Unsupported Public Key Type") diff --git a/bless/ssh/protocol/__init__.py b/bless/ssh/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/ssh/protocol/ssh_protocol.py b/bless/ssh/protocol/ssh_protocol.py new file mode 100644 index 0000000..3cf853b --- /dev/null +++ b/bless/ssh/protocol/ssh_protocol.py @@ -0,0 +1,118 @@ +""" +.. module: bless.ssh.protocol.ssh_protocol + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import binascii +import struct + + +def pack_ssh_mpint(mpint): + """ + Packs multiple precision integers. + See Section 5 of https://www.ietf.org/rfc/rfc4251.txt for more information. + :param mpint: Signed long or int to pack. + :return: An SSH string containing the mpint in two's complement format. + """ + if mpint != 0: + hex_digits = _hex_characters_length(mpint) + format_string = "0{:d}x".format(hex_digits) + + # Take the 2's complement of negative numbers. + # If it was needed, this will result in a leading 0xFF + if mpint < 0: + # hex_digits * 4 = number of bits. + mpint += 1 << (hex_digits * 4) + + # If the results needed an extra byte of padding, this will provide a leading 0x00 + hex_mpint = format(mpint, format_string) + bytes = binascii.unhexlify(hex_mpint) + else: + # Per RFC4251 a 0 value mpint results in a null string. + bytes = '' + + ret = pack_ssh_string(bytes) + + return ret + + +def pack_ssh_string(string): + """ + Packs arbitrary length binary strings. + See Section 5 of https://www.ietf.org/rfc/rfc4251.txt for more information. + :param string: String or Unicode string. Unicode is encoded as utf-8. + :return: An SSH String stored as a unint32 representing the length of the input string, + followed by that many bytes. + """ + if isinstance(string, unicode): + string = string.encode('utf-8') + + str_len = len(string) + + if len(string) > 4294967295: + raise ValueError("String must be less than 2^32 bytes long") + + return struct.pack('>I{}s'.format(str_len), str_len, string) + + +def pack_ssh_uint64(i): + """ + Packs a 64-bit unsigned integer. + :param i: integer or long. + :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") + elif i.bit_length() > 64: + raise ValueError("Must be a 64bit value") + + return struct.pack('>Q', i) + + +def pack_ssh_uint32(i): + """ + Packs a 32-bit unsigned integer. + :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") + elif i.bit_length() > 32: + raise ValueError("Must be a 32bit value") + + return struct.pack('>I', i) + + +def _hex_characters_length(mpint): + """ + Subroutine for pack_ssh_mpint. + :param mpint: Signed long or int to pack. + :return: The number of hex characters needed to represent a multiple precision integer. + """ + if mpint == 0: + return 0 + + # how many bytes? + num_bits = mpint.bit_length() + num_bytes = num_bits / 8 + + # if there are remaining bits, we need an extra byte + if num_bits % 8: + num_bytes += 1 + + # What is the highest bit in the highest byte? + shift = (num_bytes * 8) - 1 + mask = 1 << shift + + if mpint > 0: + if mpint & mask: + # if the mpint is positive, and the MSB of the highest byte is set, + # pack_ssh_mpint will need to pad with a leading 0x00 + num_bytes += 1 + else: + if not mpint & mask: + # if the mpint is negative, and the MSB of the highest byte is not set, + # pack_ssh_mpint will need pad with a leading 0xFF + num_bytes += 1 + + return num_bytes * 2 diff --git a/bless/ssh/public_keys/__init__.py b/bless/ssh/public_keys/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/ssh/public_keys/rsa_public_key.py b/bless/ssh/public_keys/rsa_public_key.py new file mode 100644 index 0000000..906cf60 --- /dev/null +++ b/bless/ssh/public_keys/rsa_public_key.py @@ -0,0 +1,46 @@ +""" +.. module: bless.ssh.public_keys.rsa_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.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicNumbers + + +class RSAPublicKey(SSHPublicKey): + def __init__(self, ssh_public_key): + """ + Extracts the useful RSA Public Key information from an SSH Public Key file. + :param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-rsa AAAAB3NzaC1yc2E..'). + """ + super(RSAPublicKey, self).__init__() + + self.type = SSHPublicKeyType.RSA + + 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 = '' + + public_key = serialization.load_ssh_public_key(ssh_public_key, 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") + + self.e = ca_pub_numbers.e + self.n = ca_pub_numbers.n + + key_bytes = base64.b64decode(split_ssh_public_key[1]) + fingerprint = hashlib.md5(key_bytes).hexdigest() + + self.fingerprint = 'RSA ' + ':'.join( + fingerprint[i:i + 2] for i in range(0, len(fingerprint), 2)) diff --git a/bless/ssh/public_keys/ssh_public_key.py b/bless/ssh/public_keys/ssh_public_key.py new file mode 100644 index 0000000..6cf71cd --- /dev/null +++ b/bless/ssh/public_keys/ssh_public_key.py @@ -0,0 +1,23 @@ +""" +.. module: bless.ssh.public_keys.ssh_public_key + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" + + +class SSHPublicKeyType(object): + RSA = 'ssh-rsa' + ED25519 = 'ssh-ed25519' + # todo support more key types + + +# todo real abstract classes +class SSHPublicKey(object): + """ + Extracts the useful Public Key information from an SSH Public Key file. + :param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-XXX AAAA....'). + """ + def __init__(self): + self.type = None + self.key_comment = None + self.fingerprint = None diff --git a/bless/ssh/public_keys/ssh_public_key_factory.py b/bless/ssh/public_keys/ssh_public_key_factory.py new file mode 100644 index 0000000..f630b0b --- /dev/null +++ b/bless/ssh/public_keys/ssh_public_key_factory.py @@ -0,0 +1,19 @@ +""" +.. module: bless.ssh.public_keys.ssh_public_key_factory + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType + + +def get_ssh_public_key(ssh_public_key): + """ + Returns the proper SSHPublicKey instance based off of the SSH Public Key file. + :param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-XXX AAAA....'). + :return: An SSHPublicKey instance. + """ + if ssh_public_key.startswith(SSHPublicKeyType.RSA): + return RSAPublicKey(ssh_public_key) + else: + raise TypeError("Unsupported Public Key Type") diff --git a/bless_client/__init__.py b/bless_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py new file mode 100755 index 0000000..f6ef35c --- /dev/null +++ b/bless_client/bless_client.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +"""bless_client + +Usage: + bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username bastion_source_ip bastion_command + +""" +import base64 +import json +import sys + +import boto3 +import os + +def main(argv): + if len(argv) != 9: + print ( + 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username bastion_source_ip bastion_command ') + return -1 + + if 'AWS_SECRET_ACCESS_KEY' not in os.environ: + print ('You need AWS credentials in your environment') + return -1 + + region = argv[0] + + with open(argv[7], 'r') as f: + public_key = f.read() + + payload = {'bastion_user': argv[2], 'bastion_user_ip': argv[3], 'remote_username': argv[4], + 'bastion_ip': argv[5], + 'command': argv[6], 'public_key_to_sign': public_key} + payload_json = json.dumps(payload) + + print('Executing:') + lambda_client = boto3.client('lambda', region_name=region) + response = lambda_client.invoke(FunctionName=argv[1], InvocationType='RequestResponse', + LogType='Tail', Payload=payload_json) + print('{}\n\n{}'.format(response['ResponseMetadata'], base64.b64decode(response['LogResult']))) + + if response['StatusCode'] != 200: + print ('Error creating cert.') + return -1 + + cert = response['Payload'].read() + + with open(argv[8], 'w') as cert_file: + cert_file.write(cert[1:len(cert) - 3]) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/bless_logo.png b/bless_logo.png new file mode 100644 index 0000000..c427b45 Binary files /dev/null and b/bless_logo.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ecf975e --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +-e . \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a7c505c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,12 @@ +[pytest] +python_files=test*.py +addopts=--tb=native -p no:doctest +norecursedirs=docs htmlcov .* {args} + +[flake8] +ignore = F999,E501,E128,E124,E402,W503,E731,F841 +max-line-length = 100 +exclude = .tox,.git,docs/* + +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3887c73 --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +import os + +from setuptools import setup + +ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) + +about = {} +with open(os.path.join(ROOT, "bless", "__about__.py")) as f: + exec (f.read(), about) + +setup( + name=about["__title__"], + version=about["__version__"], + author=about["__author__"], + author_email=about["__email__"], + url=about["__uri__"], + description=about["__summary__"], + license=about["__license__"], + packages=[], + install_requires=[ + 'boto3==1.3.1', + 'botocore==1.4.15', + 'cffi==1.6.0', + 'cryptography==1.3.1', + 'docutils==0.12', + 'enum34==1.1.4', + 'futures==3.0.5', + 'idna==2.1', + 'ipaddress==1.0.16', + 'jmespath==0.9.0', + 'marshmallow==2.7.2', + 'pyasn1==0.1.9', + 'pycparser==2.14', + 'python-dateutil==2.5.3', + 'six==1.10.0' + ], + extras_require={ + 'tests': [ + 'coverage==4.0.3', + 'flake8==2.5.4', + 'mccabe==0.4.0', + 'pep8==1.7.0', + 'py==1.4.31', + 'pyflakes==1.0.0', + 'pytest==2.9.1' + ] + } +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/aws_lambda/__init__.py b/tests/aws_lambda/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/aws_lambda/bless-test-broken.cfg b/tests/aws_lambda/bless-test-broken.cfg new file mode 100644 index 0000000..606697c --- /dev/null +++ b/tests/aws_lambda/bless-test-broken.cfg @@ -0,0 +1,5 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/not-found.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test diff --git a/tests/aws_lambda/bless-test.cfg b/tests/aws_lambda/bless-test.cfg new file mode 100644 index 0000000..8937da2 --- /dev/null +++ b/tests/aws_lambda/bless-test.cfg @@ -0,0 +1,28 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +# 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 \ No newline at end of file diff --git a/tests/aws_lambda/only-use-for-unit-tests.pem b/tests/aws_lambda/only-use-for-unit-tests.pem new file mode 100644 index 0000000..a90d2ae --- /dev/null +++ b/tests/aws_lambda/only-use-for-unit-tests.pem @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,C534195782F2DD74B6218FFF4D3F7576 + +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 +-----END RSA PRIVATE KEY----- diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py new file mode 100644 index 0000000..21763ba --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda.py @@ -0,0 +1,63 @@ +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_username": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ip": "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(): + cert = 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 cert.startswith('ssh-rsa-cert-v01@openssh.com ') + + +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(): + invalid_key_request = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ip": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" + } + with pytest.raises(TypeError): + lambda_handler(invalid_key_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')) diff --git a/tests/config/__init__.py b/tests/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/config/full.cfg b/tests/config/full.cfg new file mode 100644 index 0000000..ab44d08 --- /dev/null +++ b/tests/config/full.cfg @@ -0,0 +1,12 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG + +[Bless CA] +kms_key_id = alias/foo +us-east-1_password = +us-west-2_password = +ca_private_key_file = \ No newline at end of file diff --git a/tests/config/minimal.cfg b/tests/config/minimal.cfg new file mode 100644 index 0000000..b0ad399 --- /dev/null +++ b/tests/config/minimal.cfg @@ -0,0 +1,5 @@ +[Bless CA] +kms_key_id = alias/foo +us-west-2_password = +us-west-2_kms_context = {'insert': 'your context for us-west-2'} +ca_private_key_file = \ No newline at end of file diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py new file mode 100644 index 0000000..db56260 --- /dev/null +++ b/tests/config/test_bless_config.py @@ -0,0 +1,48 @@ +import os + +import pytest + +from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_WINDOW_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 + + +def test_empty_config(): + with pytest.raises(ValueError): + BlessConfig('us-west-2', config_file='') + + +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 Password Provided.' == e.value.message + + +@pytest.mark.parametrize( + "config,region,expected_cert_valid,expected_entropy_min,expected_rand_seed,expected_log_level," + "expected_password", [ + ((os.path.join(os.path.dirname(__file__), 'minimal.cfg')), 'us-west-2', + CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, + LOGGING_LEVEL_DEFAULT, + ''), + ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-west-2', + 1, 2, 3, 'DEBUG', + ''), + ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', + 1, 2, 3, 'DEBUG', + '') + ]) +def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, + expected_log_level, expected_password): + config = BlessConfig(region, config_file=config) + assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_WINDOW_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_log_level == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) + assert expected_password == config.getpassword() diff --git a/tests/request/__init__.py b/tests/request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py new file mode 100644 index 0000000..39eb5eb --- /dev/null +++ b/tests/request/test_bless_request.py @@ -0,0 +1,36 @@ +import pytest +from bless.request.bless_request import validate_ip, validate_user +from marshmallow import ValidationError + + +def test_validate_ip(): + validate_ip(u'127.0.0.1') + with pytest.raises(ValidationError): + validate_ip(u'256.0.0.0') + + +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') +]) +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) \ No newline at end of file diff --git a/tests/ssh/__init__.py b/tests/ssh/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ssh/test_ssh_certificate_authority_factory.py b/tests/ssh/test_ssh_certificate_authority_factory.py new file mode 100644 index 0000000..9aad05c --- /dev/null +++ b/tests/ssh/test_ssh_certificate_authority_factory.py @@ -0,0 +1,42 @@ +import pytest + +from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority +from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ + get_ssh_certificate_authority +from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType +from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ + RSA_CA_SSH_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED + + +def test_valid_key_valid_password(): + ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) + assert isinstance(ca, RSACertificateAuthority) + assert SSHPublicKeyType.RSA == ca.public_key_type + assert 65537 == ca.e + assert ca.get_signature_key() == RSA_CA_SSH_PUBLIC_KEY + + +def test_valid_key_not_encrypted(): + ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED) + assert SSHPublicKeyType.RSA == ca.public_key_type + assert 65537 == ca.e + + +def test_valid_key_missing_password(): + with pytest.raises(TypeError): + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY) + + +def test_valid_key_invalid_password(): + with pytest.raises(ValueError): + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, 'bogus') + + +def test_valid_key_not_encrypted_invalid_pass(): + with pytest.raises(TypeError): + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED, 'bogus') + + +def test_invalid_key(): + with pytest.raises(TypeError): + get_ssh_certificate_authority('bogus') diff --git a/tests/ssh/test_ssh_certificate_builder_factory.py b/tests/ssh/test_ssh_certificate_builder_factory.py new file mode 100644 index 0000000..a9ce14b --- /dev/null +++ b/tests/ssh/test_ssh_certificate_builder_factory.py @@ -0,0 +1,30 @@ +import pytest + +from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ + get_ssh_certificate_authority +from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder, \ + SSHCertifiedKeyType +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, \ + EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY + + +def test_valid_rsa_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_RSA_PUBLIC_KEY) + cert = cert_builder.get_cert_file() + assert isinstance(cert_builder, RSACertificateBuilder) + 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_invalid_key_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, 'bogus') diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py new file mode 100644 index 0000000..8b2da33 --- /dev/null +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -0,0 +1,219 @@ +import base64 + +import pytest +from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority +from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder +from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType +from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from cryptography.hazmat.primitives.serialization import _read_next_string +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, \ + RSA_USER_CERT_MANY_PRINCIPALS, RSA_HOST_CERT_MANY_PRINCIPALS, \ + 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 + +USER1 = 'user1' + + +def get_basic_public_key(public_key): + return RSAPublicKey(public_key) + + +def get_basic_rsa_ca(): + return RSACertificateAuthority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) + + +def get_basic_cert_builder_rsa(cert_type=SSHCertificateType.USER, + public_key=EXAMPLE_RSA_PUBLIC_KEY): + ca = get_basic_rsa_ca() + pub_key = get_basic_public_key(public_key) + return RSACertificateBuilder(ca, cert_type, pub_key) + + +def extract_nonce_from_cert(cert_file): + cert = cert_file.split(' ')[1] + cert_type, cert_remainder = _read_next_string(base64.b64decode(cert)) + nonce, cert_remainder = _read_next_string(cert_remainder) + return nonce + + +def test_valid_principals(): + USER2 = 'second_user' + + cert = get_basic_cert_builder_rsa() + + # No principals by default + assert list() == cert.valid_principals + + # Two principals + cert.add_valid_principal(USER1) + cert.add_valid_principal(USER2) + assert [USER1, USER2] == cert.valid_principals + + # Adding a null principal should throw a ValueError + with pytest.raises(ValueError): + cert.add_valid_principal('') + + # Adding same principal twice should not change the list, and throw a ValueError + with pytest.raises(ValueError): + cert.add_valid_principal(USER1) + assert [USER1, USER2] == cert.valid_principals + + +def test_serialize_no_principals(): + cert = get_basic_cert_builder_rsa() + + assert list() == cert.valid_principals + assert '' == cert._serialize_valid_principals() + + +def test_serialize_one_principal(): + expected = base64.b64decode('AAAABXVzZXIx') + + cert = get_basic_cert_builder_rsa() + cert.add_valid_principal(USER1) + + assert expected == cert._serialize_valid_principals() + + +def test_serialize_multiple_principals(): + users = 'user1,user2,other_user1,other_user2' + expected = base64.b64decode('AAAABXVzZXIxAAAABXVzZXIyAAAAC290aGVyX3VzZXIxAAAAC290aGVyX3VzZXIy') + + cert = get_basic_cert_builder_rsa() + for user in users.split(','): + cert.add_valid_principal(user) + + assert expected == cert._serialize_valid_principals() + + +def test_no_extensions(): + cert_builder = get_basic_cert_builder_rsa() + assert cert_builder.extensions is None + + cert_builder.clear_extensions() + assert '' == 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_after(100) + cert_builder.set_valid_after(99) + cert_builder._validate_cert_properties() + + +def test_bogus_critical_options(): + cert_builder = get_basic_cert_builder_rsa() + with pytest.raises(ValueError): + cert_builder.set_critical_option_force_command('') + + with pytest.raises(ValueError): + cert_builder.set_critical_option_source_address('') + + +def test_rsa_user_cert_minimal(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce(nonce=extract_nonce_from_cert(RSA_USER_CERT_MINIMAL)) + cert_builder.clear_extensions() + cert = cert_builder.get_cert_file() + assert RSA_USER_CERT_MINIMAL == cert + + +def test_default_extensions(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_extensions_to_default() + assert SSH_CERT_DEFAULT_EXTENSIONS == cert_builder._serialize_extensions() + +def test_add_extensions(): + extensions = {'permit-port-forwarding', + 'permit-pty', 'permit-user-rc'} + + cert_builder = get_basic_cert_builder_rsa() + + 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() + + +def test_rsa_user_cert_defaults(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce(nonce=extract_nonce_from_cert(RSA_USER_CERT_DEFAULTS)) + cert_builder.set_key_id(RSA_USER_CERT_DEFAULTS_KEY_ID) + + cert = cert_builder.get_cert_file() + assert RSA_USER_CERT_DEFAULTS == cert + + +def test_rsa_user_cert_duplicate_signs(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce(nonce=extract_nonce_from_cert(RSA_USER_CERT_DEFAULTS)) + cert_builder.set_key_id(RSA_USER_CERT_DEFAULTS_KEY_ID) + cert_builder._sign_cert() + + cert = cert_builder.get_cert_file() + assert RSA_USER_CERT_DEFAULTS == cert + + +def test_rsa_user_cert_defaults_no_public_key_comment(): + cert_builder = get_basic_cert_builder_rsa(public_key=EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION) + cert_builder.set_nonce( + nonce=extract_nonce_from_cert(RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT)) + cert_builder.set_key_id(RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID) + + cert = cert_builder.get_cert_file() + assert RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT == cert + + +def test_rsa_user_cert_many_principals(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce(nonce=extract_nonce_from_cert(RSA_USER_CERT_MANY_PRINCIPALS)) + cert_builder.set_key_id(RSA_USER_CERT_MANY_PRINCIPALS_KEY_ID) + + principals = 'user1,user2,other_user1,other_user2' + for principal in principals.split(','): + cert_builder.add_valid_principal(principal) + + cert = cert_builder.get_cert_file() + assert RSA_USER_CERT_MANY_PRINCIPALS == cert + + +def test_rsa_host_cert_many_principals(): + cert_builder = get_basic_cert_builder_rsa(cert_type=SSHCertificateType.HOST) + cert_builder.set_nonce(nonce=extract_nonce_from_cert(RSA_HOST_CERT_MANY_PRINCIPALS)) + cert_builder.set_key_id(RSA_HOST_CERT_MANY_PRINCIPALS_KEY_ID) + + principals = 'host.example.com,192.168.1.1,host2.example.com' + for principal in principals.split(','): + cert_builder.add_valid_principal(principal) + + cert = cert_builder.get_cert_file() + assert RSA_HOST_CERT_MANY_PRINCIPALS == cert + + +def test_rsa_user_cert_critical_opt_source_address(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce( + nonce=extract_nonce_from_cert(RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS)) + cert_builder.set_key_id(RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID) + cert_builder.set_critical_option_force_command('/bin/ls') + cert_builder.set_critical_option_source_address('192.168.1.0/24') + + cert = cert_builder.get_cert_file() + + assert RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS == cert + + +def test_nonce(): + cert_builder = get_basic_cert_builder_rsa() + cert_builder.set_nonce() + + cert_builder2 = get_basic_cert_builder_rsa() + cert_builder2.set_nonce() + + assert cert_builder.nonce != cert_builder2.nonce diff --git a/tests/ssh/test_ssh_protocol.py b/tests/ssh/test_ssh_protocol.py new file mode 100644 index 0000000..79530d9 --- /dev/null +++ b/tests/ssh/test_ssh_protocol.py @@ -0,0 +1,82 @@ +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')} + + for known_input, known_answer in strings.iteritems(): + 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(): + 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(): + 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(): + 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')} + + for known_input, known_answer in uint32s.iteritems(): + 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(): + assert known_answer == pack_ssh_uint64(known_input) + + +def test_floats(): + with pytest.raises(TypeError): + pack_ssh_uint64(4.2) + + with pytest.raises(TypeError): + pack_ssh_uint32(4.2) + + +def test_uint_too_long(): + with pytest.raises(ValueError): + pack_ssh_uint64(0x1FFFFFFFFFFFFFFFF) + + with pytest.raises(ValueError): + pack_ssh_uint32(long(0x1FFFFFFFF)) + + with pytest.raises(ValueError): + pack_ssh_uint32(int(0x1FFFFFFFF)) diff --git a/tests/ssh/test_ssh_public_key_factory.py b/tests/ssh/test_ssh_public_key_factory.py new file mode 100644 index 0000000..3485bbb --- /dev/null +++ b/tests/ssh/test_ssh_public_key_factory.py @@ -0,0 +1,23 @@ +import pytest + +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 + + +def test_valid_rsa(): + pub_key = get_ssh_public_key(EXAMPLE_RSA_PUBLIC_KEY) + assert 'Test RSA User Key' == pub_key.key_comment + assert EXAMPLE_RSA_PUBLIC_KEY_N == pub_key.n + assert EXAMPLE_RSA_PUBLIC_KEY_E == pub_key.e + 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_invalid_key(): + with pytest.raises(TypeError): + get_ssh_public_key(EXAMPLE_ECDSA_PUBLIC_KEY) diff --git a/tests/ssh/test_ssh_public_key_rsa.py b/tests/ssh/test_ssh_public_key_rsa.py new file mode 100644 index 0000000..3db4720 --- /dev/null +++ b/tests/ssh/test_ssh_public_key_rsa.py @@ -0,0 +1,30 @@ +import pytest + +from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, \ + EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION, EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, \ + EXAMPLE_RSA_PUBLIC_KEY_E + + +def test_valid_key(): + pub_key = RSAPublicKey(EXAMPLE_RSA_PUBLIC_KEY) + assert 'Test RSA User Key' == pub_key.key_comment + assert EXAMPLE_RSA_PUBLIC_KEY_N == pub_key.n + assert EXAMPLE_RSA_PUBLIC_KEY_E == pub_key.e + assert 'RSA 57:3d:48:4c:65:90:30:8e:39:ba:d8:fa:d0:20:2e:6c' == pub_key.fingerprint + + +def test_valid_key_no_description(): + pub_key = RSAPublicKey(EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION) + assert '' == pub_key.key_comment + assert EXAMPLE_RSA_PUBLIC_KEY_N == pub_key.n + assert EXAMPLE_RSA_PUBLIC_KEY_E == pub_key.e + assert 'RSA 57:3d:48:4c:65:90:30:8e:39:ba:d8:fa:d0:20:2e:6c' == pub_key.fingerprint + + +def test_invalid_keys(): + with pytest.raises(TypeError): + RSAPublicKey(EXAMPLE_ECDSA_PUBLIC_KEY) + + with pytest.raises(ValueError): + RSAPublicKey('bogus') diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py new file mode 100644 index 0000000..0bc2e10 --- /dev/null +++ b/tests/ssh/vectors.py @@ -0,0 +1,43 @@ +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_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_N = 863765597390437179936880505555316900679139439563534171378010191103037256891865752287401357532386053704846357061277355058425215781749953909539037751959779297844615696266967812249814741811813214887928783792221522790304297884601112319360723158242982362152538534517792264464776081835168055835189781482682493136923038734624107701903646705547410300883750998647287587929185720484066436341331195925984075699168429357216427056893851459700813345902685631135237388578414270244970778240740795928438846416944644092277106238587968690484934416451408183365968935178112856844073906509362367924503252063127972460845602238192159061426436817335330921795349155206805425009150162732786426778585802637172182106209312890241009071807202705546749148063984549775286112624905112511951652674845607083374421597899302609662133400077385016285752279857194248901093535059948349008431744427219342487822381449072585539524484989393533507171281966556907880034930485429159724230925010429126325954683461210886022933944242055541038221737797142318602403013636630990332566384663121261896469133144198125969293719738317514483451902518238751088148376136388153900264080474983146540911501501723872658100092602399884083442609325830811845748736526156589543373619454484831196371149151 +EXAMPLE_RSA_PUBLIC_KEY_E = 65537 + +EXAMPLE_ED25519_PUBLIC_KEY = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' + +EXAMPLE_ECDSA_PUBLIC_KEY = u'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' + +# 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' +# ssh-keygen -s test-rsa-ca -O clear -I "" test-rsa-user-no-extensions.pub +RSA_USER_CERT_MINIMAL = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' +# 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 = '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 AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgjK2qHMgSOZdciBtG4HJT/cZ1Q2Rl5Sf8IBMNmrJTTuYAAAADAQABAAACAQDTuckaZVjP4tP+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/MmPLhXwAAAAAAAAAAAAAAAQAAAD5zc2gta2V5Z2VuIC1zIHRlc3QtcnNhLWNhIC1JICcnIHRlc3QtcnNhLXVzZXItYWxsLWRlZmF1bHRzLnB1YgAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAOI0JnxeJSdtUXtHu7x3MVVLdDN2iFCr8B8xCSGsdmJ+VdI2sBF0xc7l5/HZVyATgCBTt9dT7eG6zFCnQhPJUUB+uhUu9EeKwu3Pqw9mWnTXy/sa/R++yswFjU1Syxws+iBbiG9IG1BIqXY+eOuXaf+8kEFi877bjU4Nrl0U/KL2qpCJwhFh3vu5XBQ26ih/TcZVRaPyOTBVqOD0MNeVXs0JkVc/gd4mLHWgFvcIrTNmmKwR0HNSDPpYbLxol+3DbfaY9CE/sUetLA6OxO4EF38PQKPY+Ud2/yypc4uC/GT1VfEc4ALZVCZYC8Cut3hYb5ef3xdab7W4ikXIyU2vtPR/n1Ju/5nXVFX1Y0F5u0ShZkz8SI7/BLF2i4SIMiZhNTgqVj8mr2LpwSHB04m68d9GsPnD1mQQxlcfx7pbfOAjXmnTV3mQmn4TnW1KxKH0n7NJE1jvRy31n3Crs3aMJ6cuq6+gxbK4cg/X48Q16PnpuYzOiTtD5hEuu4RL4cg86Mi57DmchNwb1CsvxFJrueJB65J4efIDTqmuppDQVjkZOzH2URzRzGE2azdcQPgcV5aQQiZuiqeZ5xG9oEwB/2kNVu3hsM49ugJ9OBGHtlDoemC4YkQkJe0ejFJVc8uKOzGmXfjGcXKi21Aq5jGvEsDRb3DcuLEceOfXRpU/59OpAAACDwAAAAdzc2gtcnNhAAACANuCshq4p24CB7M+lRVyBSAQhkj0C/KQjM0RqtsLT1HaPGB3c401aus4s+UB0Igaf1FQ7XXSbPXza7xrvDXPSsUoa9ihcbtEGOXp/EoHvj6Ek9m3euoQJL2tujb8QMgwhQ19KOWteK6YxUC+vbdoBp5lWDnkpZ8WOPXAuzk6o2WlFnxL7LcjFewJcM5CQr5UWHST24j1jk5U4Zer4bZueKxCk0hamjGBRlY5MnxuYbuHmLz0gqTUgtYuA422iEvNrWPSHWUUBqkz29EUiq5MJG2GWb4h+svdN9lv8blL7BFwPd0sBAaJbR9A7voPs5ZK97YW1liYNljukeW6h4dMYNAhxCC+rYNxVI3+WSshlXZy5pF7pDb9Z9RkFW8vmpEAvlLrqJqKrTJDYZEX+0tSQvHsp/y2xwk871vkWTNA/T4LKVc3GwJboXkrZrJ+q5k3JU8o8JOIabRDfAj6DEy/xW1Lshqi3kciUhjEZYNEFyYiXB9AAz6RsUUoBDnRMjnK1XpsQ6gGsL9yX5hyUiN7g/1pHi+G+ZNj8ueV/e860YgEhxYcCzTI/Xrf3dpkbgjhVpcvk9VuKmMWtZ1Q1zfC2mh8/JSej5ewTZYRNBYAvqNG70u7ej5uuemgc7hTKzrrRFpYdcp1vTKe+K8vBCmQ7Audj/Wnw1fwBT5tAHRByIKd 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' +RSA_USER_CERT_MANY_PRINCIPALS_KEY_ID = 'ssh-keygen -s test-rsa-ca -n user1,user2,other_user1,other_user2 -I \'\' test-rsa-user-many-principals.pub' +# ssh-keygen -s test-rsa-ca -O source-address=192.168.1.0/24 -O force-command=/bin/ls -I "ssh-keygen -s test-rsa-ca -O source-address=192.168.1.0/24 -O force-command=/bin/ls -I '' test-rsa-user-force-command-and-source-address.pub" test-rsa-user-force-command-and-source-address.pub +RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' +RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID = 'ssh-keygen -s test-rsa-ca -O source-address=192.168.1.0/24 -O force-command=/bin/ls -I \'\' test-rsa-user-force-command-and-source-address.pub' +# ssh-keygen -s test-rsa-ca -h -n host.example.com,192.168.1.1,host2.example.com -I "ssh-keygen -s test-rsa-ca -h -n host.example.com,192.168.1.1,host2.example.com -I '' test-rsa-host-many-principals.pub" test-rsa-host-many-principals.pub +RSA_HOST_CERT_MANY_PRINCIPALS = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' +RSA_HOST_CERT_MANY_PRINCIPALS_KEY_ID = 'ssh-keygen -s test-rsa-ca -h -n host.example.com,192.168.1.1,host2.example.com -I \'\' test-rsa-host-many-principals.pub' + +SSH_CERT_DEFAULT_EXTENSIONS = base64.b64decode( + 'AAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') + +SSH_CERT_CUSTOM_EXTENSIONS = base64.b64decode( + 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') \ No newline at end of file