From 7a3b6fe8ef5b0de1568be320b2f19bebccc9db34 Mon Sep 17 00:00:00 2001 From: Jason Rigby Date: Wed, 27 Dec 2023 00:42:08 +0900 Subject: [PATCH] add dummy key; code cleanup; update gh actions; migrate distutils --- .github/workflows/publish.yml | 6 ++--- .github/workflows/test.yml | 7 ++--- .idea/misc.xml | 5 +++- .idea/paramiko-cloud.iml | 4 ++- paramiko_cloud/azure/keys.py | 6 ++--- paramiko_cloud/base.py | 8 +++--- paramiko_cloud/dummy/__init__.py | 0 paramiko_cloud/dummy/keys.py | 32 ++++++++++++++++++++++ paramiko_cloud/dummy/test_keys.py | 45 +++++++++++++++++++++++++++++++ setup.py | 38 +++++++++++++++----------- 10 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 paramiko_cloud/dummy/__init__.py create mode 100644 paramiko_cloud/dummy/keys.py create mode 100644 paramiko_cloud/dummy/test_keys.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2aff747..50d43bb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: with: submodules: 'true' - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.7 - name: Install Protoc @@ -35,13 +35,13 @@ jobs: --outdir dist/ . - name: Publish distribution 📦 to Test PyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: skip_existing: true password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ - name: Publish distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a3842d..a293679 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,22 +8,23 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 with: submodules: 'true' - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Protoc uses: arduino/setup-protoc@v1 - name: Install dependencies and build protobuf files run: | + pip install -U pip + pip install -U setuptools python setup.py build_proto - python -m pip install --upgrade pip pip install pytest pytest-cov pip install -e .[all] - name: Test with pytest diff --git a/.idea/misc.xml b/.idea/misc.xml index a2207eb..0541d0c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ - + + + \ No newline at end of file diff --git a/.idea/paramiko-cloud.iml b/.idea/paramiko-cloud.iml index 06b0085..faa5fb5 100644 --- a/.idea/paramiko-cloud.iml +++ b/.idea/paramiko-cloud.iml @@ -1,7 +1,9 @@ - + + + diff --git a/paramiko_cloud/azure/keys.py b/paramiko_cloud/azure/keys.py index 297b269..fa91c31 100644 --- a/paramiko_cloud/azure/keys.py +++ b/paramiko_cloud/azure/keys.py @@ -88,9 +88,9 @@ class ECDSAKey(BaseKeyECDSA): ) def __init__(self, credential: Union[DefaultAzureCredential, AzurePowerShellCredential, - InteractiveBrowserCredential, ChainedTokenCredential, EnvironmentCredential, - ManagedIdentityCredential, SharedTokenCacheCredential, AzureCliCredential, - VisualStudioCodeCredential], + InteractiveBrowserCredential, ChainedTokenCredential, EnvironmentCredential, + ManagedIdentityCredential, SharedTokenCacheCredential, AzureCliCredential, + VisualStudioCodeCredential], vault_url: str, key_name: str): vault_client = KeyClient(vault_url, credential=credential) pub_key = vault_client.get_key(key_name) diff --git a/paramiko_cloud/base.py b/paramiko_cloud/base.py index 4ed1bd2..789e1f9 100644 --- a/paramiko_cloud/base.py +++ b/paramiko_cloud/base.py @@ -26,20 +26,20 @@ def __init__(self, curve: EllipticCurve): self.curve = curve @staticmethod - def digest(data: bytes, ec: ECDSA) -> bytes: + def digest(data: bytes, signature_algorithm: ECDSA) -> bytes: """ Calculates the hash of the given data according to the given elliptic curve key Args: data: the data for which to calculate the hash - ec: the elliptic curve key that will use the hash + signature_algorithm: the elliptic curve signature algorithm Returns: The hash of the data """ - return getattr(hashlib, ec.algorithm.name)(data).digest() + return getattr(hashlib, signature_algorithm.algorithm.name)(data).digest() - def sign(self, data, signature_algorithm: ECDSA) -> bytes: + def sign(self, data: bytes, signature_algorithm: ECDSA) -> bytes: """ Calculate the signature for the given data diff --git a/paramiko_cloud/dummy/__init__.py b/paramiko_cloud/dummy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/paramiko_cloud/dummy/keys.py b/paramiko_cloud/dummy/keys.py new file mode 100644 index 0000000..08ee321 --- /dev/null +++ b/paramiko_cloud/dummy/keys.py @@ -0,0 +1,32 @@ +from typing import Optional + +from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey, ECDSA +from cryptography.hazmat.primitives.serialization import load_pem_private_key + +from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey + + +class _LocalSigningKey(CloudSigningKey): + """ + A dummy signing key + """ + def __init__(self, key: EllipticCurvePrivateKey): + super().__init__(key.curve) + self.key = key + + def sign(self, data: bytes, signature_algorithm: ECDSA) -> bytes: + return self.key.sign(data, signature_algorithm) + + +class ECDSAKey(BaseKeyECDSA): + """ + A dummy key that demonstrates the abstraction, but just loads they key from file. + + Args: + pem_private_key: A PEM-formatted private key + password: An optional password to decrypt the private key + """ + def __init__(self, pem_private_key: bytes, password: Optional[bytes] = None): + private_key: EllipticCurvePrivateKey = load_pem_private_key(pem_private_key, password) + public_key = private_key.public_key() + super().__init__((_LocalSigningKey(private_key), public_key)) diff --git a/paramiko_cloud/dummy/test_keys.py b/paramiko_cloud/dummy/test_keys.py new file mode 100644 index 0000000..5227b6d --- /dev/null +++ b/paramiko_cloud/dummy/test_keys.py @@ -0,0 +1,45 @@ +from unittest import TestCase + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec +from paramiko.rsakey import RSAKey + +from paramiko_cloud.dummy.keys import ECDSAKey +from paramiko_cloud.test_helpers import parse_certificate, sha256_fingerprint + +private_key = ec.generate_private_key(ec.SECP256R1()).private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() +) + + +class TestECDSAKey(TestCase): + def test_key_from_cloud_can_sign(self): + key = ECDSAKey(private_key) + signature = key.sign_ssh_data(b"hello world") + signature.rewind() + self.assertTrue(key.verify_ssh_sig(b"hello world", signature), "Signature is invalid") + + def test_key_from_cloud_can_produce_valid_certificate(self): + ca_key = ECDSAKey(private_key) + client_key = RSAKey.generate(1024) + cert_string = ca_key.sign_certificate(client_key, ["test.user"]).cert_string() + exit_code, cert_details = parse_certificate(cert_string) + self.assertEqual( + cert_details.public_key, + "RSA-CERT SHA256:{}".format(sha256_fingerprint(client_key)) + ) + self.assertEqual( + cert_details.signing_ca, + "ECDSA SHA256:{} (using ecdsa-sha2-nistp{})".format( + sha256_fingerprint(ca_key), + ca_key.ecdsa_curve.key_length + ) + ) + self.assertEqual( + exit_code, 0, + "Could not parse generated certificate with ssh-keygen, exit code {}".format( + exit_code + ) + ) diff --git a/setup.py b/setup.py index 5e44e1d..25eca10 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ import ast import itertools import os.path -from distutils.cmd import Command from glob import glob from typing import List, Tuple +from setuptools import Command from setuptools import setup setup_path = os.path.dirname(os.path.realpath(__file__)) @@ -39,10 +39,10 @@ def run(self): # Build the gRPC stubs grpc_tools.protoc.main([ - 'grpc_tools.protoc', - '-I{}'.format(self.GRPC_PROTO_PATH), - '--python_out={}'.format(self.PYTHON_OUT_PATH), - '--grpc_python_out={}'.format(self.PYTHON_OUT_PATH), + "grpc_tools.protoc", + "-I{}".format(self.GRPC_PROTO_PATH), + "--python_out={}".format(self.PYTHON_OUT_PATH), + "--grpc_python_out={}".format(self.PYTHON_OUT_PATH), os.path.join(self.GRPC_PROTO_PATH, "rpc.proto") ]) @@ -81,22 +81,28 @@ def run(self): } extras_require["all"] = list(itertools.chain(*extras_require.values())) +with open("README.md", "rt") as f: + long_description = f.read() + setup( name='paramiko-cloud', - version='1.2.1', + version='1.3.0', packages=[ - 'paramiko_cloud', - 'paramiko_cloud.aws', - 'paramiko_cloud.azure', - 'paramiko_cloud.gcp', - 'paramiko_cloud.protobuf' + "paramiko_cloud", + "paramiko_cloud.dummy", + "paramiko_cloud.aws", + "paramiko_cloud.azure", + "paramiko_cloud.gcp", + "paramiko_cloud.protobuf" ], include_package_data=True, - url='https://github.com/jasonrig/paramiko-cloud/', - license='MIT', - author='Jason Rigby', - author_email='hello@jasonrig.by', - description='Use cloud-managed keys to sign SSH certificates', + url="https://github.com/jasonrig/paramiko-cloud/", + license="MIT", + author="Jason Rigby", + author_email="hello@jasonrig.by", + description="Use cloud-managed keys to sign SSH certificates", + long_description=long_description, + long_description_content_type='text/markdown', setup_requires=[ "protobuf_distutils", "grpcio-tools",