paramiko-cloud 1.3.0__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. paramiko_cloud-1.3.2/PKG-INFO +59 -0
  2. paramiko_cloud-1.3.2/README.md +33 -0
  3. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/aws/keys.py +29 -17
  4. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/aws/test_keys.py +25 -19
  5. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/azure/keys.py +59 -26
  6. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/azure/test_keys.py +63 -28
  7. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/base.py +28 -12
  8. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/dummy/keys.py +6 -2
  9. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/dummy/test_keys.py +10 -8
  10. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/gcp/keys.py +29 -8
  11. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/gcp/test_keys.py +26 -21
  12. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/grpc_server.py +17 -5
  13. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/pki.py +82 -34
  14. paramiko_cloud-1.3.2/paramiko_cloud/protobuf/__init__.py +3 -0
  15. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/protobuf/csr_pb2.py +29 -18
  16. paramiko_cloud-1.3.2/paramiko_cloud/protobuf/csr_pb2.pyi +72 -0
  17. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/protobuf/rpc_pb2.py +14 -4
  18. paramiko_cloud-1.3.2/paramiko_cloud/protobuf/rpc_pb2.pyi +51 -0
  19. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/protobuf/rpc_pb2_grpc.py +50 -9
  20. paramiko_cloud-1.3.2/paramiko_cloud/py.typed +1 -0
  21. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/test_grpc_server.py +9 -5
  22. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/test_helpers.py +11 -9
  23. paramiko_cloud-1.3.2/paramiko_cloud/test_pki.py +61 -0
  24. paramiko_cloud-1.3.2/paramiko_cloud.egg-info/PKG-INFO +59 -0
  25. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud.egg-info/SOURCES.txt +4 -1
  26. paramiko_cloud-1.3.2/pyproject.toml +77 -0
  27. paramiko-cloud-1.3.0/PKG-INFO +0 -28
  28. paramiko-cloud-1.3.0/README.md +0 -14
  29. paramiko-cloud-1.3.0/paramiko_cloud/protobuf/__init__.py +0 -0
  30. paramiko-cloud-1.3.0/paramiko_cloud/test_pki.py +0 -24
  31. paramiko-cloud-1.3.0/paramiko_cloud.egg-info/PKG-INFO +0 -28
  32. paramiko-cloud-1.3.0/setup.py +0 -128
  33. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/__init__.py +0 -0
  34. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/aws/__init__.py +0 -0
  35. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/azure/__init__.py +0 -0
  36. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/dummy/__init__.py +0 -0
  37. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud/gcp/__init__.py +0 -0
  38. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud.egg-info/dependency_links.txt +0 -0
  39. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud.egg-info/requires.txt +0 -0
  40. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/paramiko_cloud.egg-info/top_level.txt +0 -0
  41. {paramiko-cloud-1.3.0 → paramiko_cloud-1.3.2}/setup.cfg +0 -0
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: paramiko-cloud
3
+ Version: 1.3.2
4
+ Summary: Use cloud-managed keys to sign SSH certificates
5
+ Author-email: Jason Rigby <hello@jasonrig.by>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jasonrig/paramiko-cloud/
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: paramiko
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: protobuf
13
+ Requires-Dist: grpcio-tools
14
+ Provides-Extra: aws
15
+ Requires-Dist: boto3; extra == "aws"
16
+ Provides-Extra: gcp
17
+ Requires-Dist: google-cloud-kms; extra == "gcp"
18
+ Provides-Extra: azure
19
+ Requires-Dist: azure-keyvault-keys; extra == "azure"
20
+ Requires-Dist: azure-identity; extra == "azure"
21
+ Provides-Extra: all
22
+ Requires-Dist: boto3; extra == "all"
23
+ Requires-Dist: google-cloud-kms; extra == "all"
24
+ Requires-Dist: azure-keyvault-keys; extra == "all"
25
+ Requires-Dist: azure-identity; extra == "all"
26
+
27
+ # Paramiko-Cloud
28
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=security_rating)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud) [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=sqale_index)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud)
29
+
30
+ Paramiko-Cloud is an extension to Paramiko that provides ECDSA SSH keys managed by
31
+ cloud-based key management services. As well as enabling Paramiko to perform SSH
32
+ operations using cloud-managed keys, it also provides certificate signing functions,
33
+ simplifying the implementation of an SSH certificate authority.
34
+
35
+ Paramiko-Cloud supports:
36
+ * [Amazon Web Services - Key Management Service](https://aws.amazon.com/kms/)
37
+ * [Google Cloud Platform - Cloud Key Management Service](https://cloud.google.com/security-key-management)
38
+ * [Microsoft Azure - Key Vault](https://azure.microsoft.com/en-us/services/key-vault/)
39
+
40
+ Read the docs here: https://paramiko-cloud.readthedocs.io/en/latest/
41
+
42
+ ## Development with uv
43
+
44
+ ```shell
45
+ # Sync runtime + optional cloud provider dependencies + test tooling
46
+ uv sync --extra all --group dev
47
+
48
+ # Generate protobuf / gRPC Python modules
49
+ uv run python scripts/build_proto.py
50
+
51
+ # Run tests
52
+ uv run pytest --cov=./ --cov-report=xml
53
+
54
+ # Static checks (requires generated protobuf modules first)
55
+ uv run python scripts/build_proto.py
56
+ uv run ruff check .
57
+ uv run ruff format . --check
58
+ uv run mypy paramiko_cloud
59
+ ```
@@ -0,0 +1,33 @@
1
+ # Paramiko-Cloud
2
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=security_rating)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud) [![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=jasonrig_paramiko-cloud&metric=sqale_index)](https://sonarcloud.io/dashboard?id=jasonrig_paramiko-cloud)
3
+
4
+ Paramiko-Cloud is an extension to Paramiko that provides ECDSA SSH keys managed by
5
+ cloud-based key management services. As well as enabling Paramiko to perform SSH
6
+ operations using cloud-managed keys, it also provides certificate signing functions,
7
+ simplifying the implementation of an SSH certificate authority.
8
+
9
+ Paramiko-Cloud supports:
10
+ * [Amazon Web Services - Key Management Service](https://aws.amazon.com/kms/)
11
+ * [Google Cloud Platform - Cloud Key Management Service](https://cloud.google.com/security-key-management)
12
+ * [Microsoft Azure - Key Vault](https://azure.microsoft.com/en-us/services/key-vault/)
13
+
14
+ Read the docs here: https://paramiko-cloud.readthedocs.io/en/latest/
15
+
16
+ ## Development with uv
17
+
18
+ ```shell
19
+ # Sync runtime + optional cloud provider dependencies + test tooling
20
+ uv sync --extra all --group dev
21
+
22
+ # Generate protobuf / gRPC Python modules
23
+ uv run python scripts/build_proto.py
24
+
25
+ # Run tests
26
+ uv run pytest --cov=./ --cov-report=xml
27
+
28
+ # Static checks (requires generated protobuf modules first)
29
+ uv run python scripts/build_proto.py
30
+ uv run ruff check .
31
+ uv run ruff format . --check
32
+ uv run mypy paramiko_cloud
33
+ ```
@@ -1,7 +1,11 @@
1
- from typing import Set
1
+ from typing import Any, Set, cast
2
2
 
3
3
  import boto3
4
- from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurve
4
+ from cryptography.hazmat.primitives.asymmetric.ec import (
5
+ ECDSA,
6
+ EllipticCurve,
7
+ EllipticCurvePublicKey,
8
+ )
5
9
  from cryptography.hazmat.primitives.serialization import load_der_public_key
6
10
 
7
11
  from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey
@@ -12,7 +16,9 @@ class _AWSSigningKey(CloudSigningKey):
12
16
  Provides signing operations to Paramiko for the AWS KMS-backed key
13
17
  """
14
18
 
15
- def __init__(self, client, key_id: str, signing_algo: str, curve: EllipticCurve):
19
+ def __init__(
20
+ self, client: Any, key_id: str, signing_algo: str, curve: EllipticCurve
21
+ ) -> None:
16
22
  """
17
23
  Constructor
18
24
 
@@ -46,9 +52,9 @@ class _AWSSigningKey(CloudSigningKey):
46
52
  Message=hash_,
47
53
  MessageType="DIGEST",
48
54
  GrantTokens=[
49
- 'string',
55
+ "string",
50
56
  ],
51
- SigningAlgorithm=self.signing_algo
57
+ SigningAlgorithm=self.signing_algo,
52
58
  )
53
59
  return signing_response["Signature"]
54
60
 
@@ -71,31 +77,35 @@ class ECDSAKey(BaseKeyECDSA):
71
77
  ("ECC_NIST_P521", "ECDSA_SHA_512"),
72
78
  )
73
79
 
74
- def __init__(self, key_id: str, **kwargs):
75
- client = boto3.client('kms', **kwargs)
80
+ def __init__(self, key_id: str, **kwargs: Any) -> None:
81
+ client = boto3.client("kms", **kwargs)
76
82
  pub_key = client.get_public_key(
77
83
  KeyId=key_id,
78
84
  GrantTokens=[
79
- 'string',
80
- ]
85
+ "string",
86
+ ],
81
87
  )
82
88
 
83
89
  assert any(
84
- signing_algo in pub_key["SigningAlgorithms"] and
85
- key_spec == pub_key["CustomerMasterKeySpec"]
86
- for key_spec, signing_algo in self._ALLOWED_ALGOS), \
87
- "No supported key/algorithm pair found."
90
+ signing_algo in pub_key["SigningAlgorithms"]
91
+ and key_spec == pub_key["CustomerMasterKeySpec"]
92
+ for key_spec, signing_algo in self._ALLOWED_ALGOS
93
+ ), "No supported key/algorithm pair found."
88
94
  assert pub_key["KeyUsage"] == "SIGN_VERIFY", "Key does not support signing."
89
95
 
90
- verifying_key = load_der_public_key(pub_key["PublicKey"])
96
+ verifying_key = cast(
97
+ EllipticCurvePublicKey, load_der_public_key(pub_key["PublicKey"])
98
+ )
91
99
 
92
100
  super().__init__(
93
101
  (
94
102
  _AWSSigningKey(
95
103
  client,
96
104
  key_id,
97
- self._choose_signing_algo(set(pub_key["SigningAlgorithms"])), verifying_key.curve),
98
- verifying_key
105
+ self._choose_signing_algo(set(pub_key["SigningAlgorithms"])),
106
+ verifying_key.curve,
107
+ ),
108
+ verifying_key,
99
109
  )
100
110
  )
101
111
 
@@ -110,4 +120,6 @@ class ECDSAKey(BaseKeyECDSA):
110
120
  The selected signing algorithm
111
121
  """
112
122
 
113
- return set([algo for _, algo in self._ALLOWED_ALGOS]).intersection(supported_algos).pop()
123
+ return {algo for _, algo in self._ALLOWED_ALGOS}.intersection(
124
+ supported_algos
125
+ ).pop()
@@ -16,7 +16,7 @@ def set_up_mocks(boto3_mock: Mock, curve: EllipticCurve, hash_algo: HashAlgorith
16
16
  priv_key = ec.generate_private_key(curve)
17
17
  pem_key = priv_key.public_key().public_bytes(
18
18
  encoding=serialization.Encoding.DER,
19
- format=serialization.PublicFormat.SubjectPublicKeyInfo
19
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
20
20
  )
21
21
 
22
22
  signing_algorithm_name = "ECDSA_SHA_{}".format(hash_algo.name[-3:])
@@ -27,17 +27,12 @@ def set_up_mocks(boto3_mock: Mock, curve: EllipticCurve, hash_algo: HashAlgorith
27
27
  "SigningAlgorithms": [signing_algorithm_name],
28
28
  "CustomerMasterKeySpec": key_spec_name,
29
29
  "KeyUsage": "SIGN_VERIFY",
30
- "PublicKey": pem_key
30
+ "PublicKey": pem_key,
31
31
  }
32
32
 
33
33
  def sign(KeyId, Message, MessageType, GrantTokens, SigningAlgorithm):
34
34
  assert SigningAlgorithm == signing_algorithm_name
35
- return {
36
- "Signature": priv_key.sign(
37
- Message,
38
- ec.ECDSA(Prehashed(hash_algo))
39
- )
40
- }
35
+ return {"Signature": priv_key.sign(Message, ec.ECDSA(Prehashed(hash_algo)))}
41
36
 
42
37
  boto3_instance.sign.side_effect = sign
43
38
 
@@ -46,44 +41,55 @@ class TestECDSAKey(TestCase):
46
41
  ALL_SUPPORTED_ALGOS: Tuple[Tuple[EllipticCurve, HashAlgorithm]] = (
47
42
  (ec.SECP256R1(), hashes.SHA256()),
48
43
  (ec.SECP384R1(), hashes.SHA384()),
49
- (ec.SECP521R1(), hashes.SHA512())
44
+ (ec.SECP521R1(), hashes.SHA512()),
50
45
  )
51
46
 
52
47
  @patch("boto3.client")
53
48
  def test_key_from_cloud_can_sign(self, boto3_mock: Mock):
54
49
  from paramiko_cloud.aws.keys import ECDSAKey
50
+
55
51
  for curve, hash_ in self.ALL_SUPPORTED_ALGOS:
56
- with self.subTest("Using curve {} and hash {}".format(curve.name, hash_.name)):
52
+ with self.subTest(
53
+ "Using curve {} and hash {}".format(curve.name, hash_.name)
54
+ ):
57
55
  set_up_mocks(boto3_mock, curve, hash_)
58
56
  key = ECDSAKey("test_key", region_name="ap-northeast-1")
59
57
  signature = key.sign_ssh_data(b"hello world")
60
58
  signature.rewind()
61
- self.assertTrue(key.verify_ssh_sig(b"hello world", signature), "Signature is invalid")
59
+ self.assertTrue(
60
+ key.verify_ssh_sig(b"hello world", signature),
61
+ "Signature is invalid",
62
+ )
62
63
 
63
64
  @patch("boto3.client")
64
65
  def test_key_from_cloud_can_produce_valid_certificate(self, boto3_mock: Mock):
65
66
  from paramiko_cloud.aws.keys import ECDSAKey
67
+
66
68
  for curve, hash_ in self.ALL_SUPPORTED_ALGOS:
67
- with self.subTest("Using curve {} and hash {}".format(curve.name, hash_.name)):
69
+ with self.subTest(
70
+ "Using curve {} and hash {}".format(curve.name, hash_.name)
71
+ ):
68
72
  set_up_mocks(boto3_mock, curve, hash_)
69
73
  ca_key = ECDSAKey("test_key", region_name="ap-northeast-1")
70
74
  client_key = RSAKey.generate(1024)
71
- cert_string = ca_key.sign_certificate(client_key, ["test.user"]).cert_string()
75
+ cert_string = ca_key.sign_certificate(
76
+ client_key, ["test.user"]
77
+ ).cert_string()
72
78
  exit_code, cert_details = parse_certificate(cert_string)
73
79
  self.assertEqual(
74
80
  cert_details.public_key,
75
- "RSA-CERT SHA256:{}".format(sha256_fingerprint(client_key))
81
+ "RSA-CERT SHA256:{}".format(sha256_fingerprint(client_key)),
76
82
  )
77
83
  self.assertEqual(
78
84
  cert_details.signing_ca,
79
85
  "ECDSA SHA256:{} (using ecdsa-sha2-nistp{})".format(
80
- sha256_fingerprint(ca_key),
81
- ca_key.ecdsa_curve.key_length
82
- )
86
+ sha256_fingerprint(ca_key), ca_key.ecdsa_curve.key_length
87
+ ),
83
88
  )
84
89
  self.assertEqual(
85
- exit_code, 0,
90
+ exit_code,
91
+ 0,
86
92
  "Could not parse generated certificate with ssh-keygen, exit code {}".format(
87
93
  exit_code
88
- )
94
+ ),
89
95
  )
@@ -1,12 +1,28 @@
1
- from typing import Union
2
-
3
- from azure.identity import DefaultAzureCredential, AzurePowerShellCredential, InteractiveBrowserCredential, \
4
- ChainedTokenCredential, EnvironmentCredential, ManagedIdentityCredential, SharedTokenCacheCredential, \
5
- AzureCliCredential, VisualStudioCodeCredential
1
+ from typing import Union, cast
2
+
3
+ from azure.identity import (
4
+ DefaultAzureCredential,
5
+ AzurePowerShellCredential,
6
+ InteractiveBrowserCredential,
7
+ ChainedTokenCredential,
8
+ EnvironmentCredential,
9
+ ManagedIdentityCredential,
10
+ SharedTokenCacheCredential,
11
+ AzureCliCredential,
12
+ VisualStudioCodeCredential,
13
+ )
6
14
  from azure.keyvault.keys import KeyClient
7
15
  from azure.keyvault.keys.crypto import CryptographyClient, SignatureAlgorithm
8
- from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePublicNumbers, SECP256R1, \
9
- SECP384R1, SECP521R1, SECP224R1, SECP192R1, EllipticCurve
16
+ from cryptography.hazmat.primitives.asymmetric.ec import (
17
+ ECDSA,
18
+ EllipticCurvePublicNumbers,
19
+ SECP256R1,
20
+ SECP384R1,
21
+ SECP521R1,
22
+ SECP224R1,
23
+ SECP192R1,
24
+ EllipticCurve,
25
+ )
10
26
  from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
11
27
 
12
28
  from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey
@@ -65,8 +81,8 @@ class _AzureSigningKey(CloudSigningKey):
65
81
  digest = self.digest(data, signature_algorithm)
66
82
  signing_response = self.crypto_client.sign(self._signaure_algorithm(), digest)
67
83
  raw_signature = signing_response.signature
68
- r = int.from_bytes(raw_signature[:len(raw_signature) // 2], "big")
69
- s = int.from_bytes(raw_signature[len(raw_signature) // 2:], "big")
84
+ r = int.from_bytes(raw_signature[: len(raw_signature) // 2], "big")
85
+ s = int.from_bytes(raw_signature[len(raw_signature) // 2 :], "big")
70
86
  return encode_dss_signature(r, s)
71
87
 
72
88
 
@@ -83,32 +99,49 @@ class ECDSAKey(BaseKeyECDSA):
83
99
  https://docs.microsoft.com/en-us/azure/developer/python/azure-sdk-authenticate
84
100
  """
85
101
 
86
- _ALLOWED_ALGOS = (
87
- "EC",
88
- )
89
-
90
- def __init__(self, credential: Union[DefaultAzureCredential, AzurePowerShellCredential,
91
- InteractiveBrowserCredential, ChainedTokenCredential, EnvironmentCredential,
92
- ManagedIdentityCredential, SharedTokenCacheCredential, AzureCliCredential,
93
- VisualStudioCodeCredential],
94
- vault_url: str, key_name: str):
102
+ _ALLOWED_ALGOS = ("EC",)
103
+
104
+ def __init__(
105
+ self,
106
+ credential: Union[
107
+ DefaultAzureCredential,
108
+ AzurePowerShellCredential,
109
+ InteractiveBrowserCredential,
110
+ ChainedTokenCredential,
111
+ EnvironmentCredential,
112
+ ManagedIdentityCredential,
113
+ SharedTokenCacheCredential,
114
+ AzureCliCredential,
115
+ VisualStudioCodeCredential,
116
+ ],
117
+ vault_url: str,
118
+ key_name: str,
119
+ ):
95
120
  vault_client = KeyClient(vault_url, credential=credential)
96
121
  pub_key = vault_client.get_key(key_name)
97
- assert pub_key.key_type in self._ALLOWED_ALGOS, "Unsupported signing algorithm: {}".format(pub_key.key_type)
122
+ assert pub_key.key_type in self._ALLOWED_ALGOS, (
123
+ "Unsupported signing algorithm: {}".format(pub_key.key_type)
124
+ )
125
+
126
+ jwk = pub_key.key
127
+ assert jwk is not None, "Missing key material from Azure Key Vault."
128
+ curve_name = cast(str, getattr(jwk, "crv"))
98
129
 
99
- assert pub_key.key.crv in _CURVES, "Unsupported curve: {}".format(pub_key.key.crv)
130
+ assert curve_name in _CURVES, "Unsupported curve: {}".format(curve_name)
100
131
 
101
- curve = _CURVES[pub_key.key.crv]()
132
+ curve = _CURVES[curve_name]() # type: ignore[abstract]
102
133
 
103
134
  verifying_key = EllipticCurvePublicNumbers(
104
- int.from_bytes(pub_key.key.x, "big"),
105
- int.from_bytes(pub_key.key.y, "big"),
106
- curve
135
+ int.from_bytes(cast(bytes, getattr(jwk, "x")), "big"),
136
+ int.from_bytes(cast(bytes, getattr(jwk, "y")), "big"),
137
+ curve,
107
138
  ).public_key()
108
139
 
109
140
  super().__init__(
110
141
  (
111
- _AzureSigningKey(CryptographyClient(pub_key, credential), verifying_key.curve),
112
- verifying_key
142
+ _AzureSigningKey(
143
+ CryptographyClient(pub_key, credential), verifying_key.curve
144
+ ),
145
+ verifying_key,
113
146
  )
114
147
  )
@@ -6,7 +6,10 @@ from azure.keyvault.keys import KeyVaultKey
6
6
  from cryptography.hazmat.primitives import hashes
7
7
  from cryptography.hazmat.primitives.asymmetric import ec
8
8
  from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
9
- from cryptography.hazmat.primitives.asymmetric.utils import Prehashed, decode_dss_signature
9
+ from cryptography.hazmat.primitives.asymmetric.utils import (
10
+ Prehashed,
11
+ decode_dss_signature,
12
+ )
10
13
  from cryptography.hazmat.primitives.hashes import HashAlgorithm
11
14
  from paramiko.rsakey import RSAKey
12
15
 
@@ -18,77 +21,109 @@ class _SigningResponse:
18
21
  self.signature = signature
19
22
 
20
23
 
21
- def set_up_mocks(key_client_mock: Mock, crypto_client_mock: Mock, curve: EllipticCurve, hash_algo: HashAlgorithm,
22
- key_name: str):
24
+ def set_up_mocks(
25
+ key_client_mock: Mock,
26
+ crypto_client_mock: Mock,
27
+ curve: EllipticCurve,
28
+ hash_algo: HashAlgorithm,
29
+ key_name: str,
30
+ ):
23
31
  priv_key = ec.generate_private_key(curve)
24
32
  pub_key = priv_key.public_key().public_numbers()
25
33
 
26
34
  crypto_client_instance = crypto_client_mock.return_value
27
35
 
28
36
  def sign(_, digest):
29
- signature = priv_key.sign(
30
- digest,
31
- ec.ECDSA(Prehashed(hash_algo))
32
- )
37
+ signature = priv_key.sign(digest, ec.ECDSA(Prehashed(hash_algo)))
33
38
  r, s = decode_dss_signature(signature)
34
39
  return _SigningResponse(r.to_bytes(128, "big") + s.to_bytes(128, "big"))
35
40
 
36
41
  crypto_client_instance.sign.side_effect = sign
37
42
  key_client_instance = key_client_mock.return_value
38
- key_client_instance.get_key.return_value = KeyVaultKey(key_name, jwk={
39
- "kty": "EC",
40
- "crv": "P-{}".format(curve.key_size),
41
- "x": int.to_bytes(pub_key.x, 128, "big"),
42
- "y": int.to_bytes(pub_key.y, 128, "big")
43
- })
43
+ key_client_instance.get_key.return_value = KeyVaultKey(
44
+ key_name,
45
+ jwk={
46
+ "kty": "EC",
47
+ "crv": "P-{}".format(curve.key_size),
48
+ "x": int.to_bytes(pub_key.x, 128, "big"),
49
+ "y": int.to_bytes(pub_key.y, 128, "big"),
50
+ },
51
+ )
44
52
 
45
53
 
46
54
  class TestECDSAKey(TestCase):
47
55
  ALL_SUPPORTED_ALGOS: Tuple[Tuple[EllipticCurve, HashAlgorithm]] = (
48
56
  (ec.SECP256R1(), hashes.SHA256()),
49
57
  (ec.SECP384R1(), hashes.SHA384()),
50
- (ec.SECP521R1(), hashes.SHA512())
58
+ (ec.SECP521R1(), hashes.SHA512()),
51
59
  )
52
60
 
53
61
  TEST_KEY_NAME = "https://test-vault.vault.azure.net/keys/abc/1"
54
62
 
55
63
  @patch("azure.keyvault.keys.crypto.CryptographyClient")
56
64
  @patch("azure.keyvault.keys.KeyClient")
57
- def test_key_from_cloud_can_sign(self, key_client_mock: Mock, crypto_client_mock: Mock):
65
+ def test_key_from_cloud_can_sign(
66
+ self, key_client_mock: Mock, crypto_client_mock: Mock
67
+ ):
58
68
  from paramiko_cloud.azure.keys import ECDSAKey
69
+
59
70
  for curve, hash_ in self.ALL_SUPPORTED_ALGOS:
60
- with self.subTest("Using curve {} and hash {}".format(curve.name, hash_.name)):
61
- set_up_mocks(key_client_mock, crypto_client_mock, curve, hash_, self.TEST_KEY_NAME)
71
+ with self.subTest(
72
+ "Using curve {} and hash {}".format(curve.name, hash_.name)
73
+ ):
74
+ set_up_mocks(
75
+ key_client_mock,
76
+ crypto_client_mock,
77
+ curve,
78
+ hash_,
79
+ self.TEST_KEY_NAME,
80
+ )
62
81
  key = ECDSAKey(None, None, "test_key")
63
82
  signature = key.sign_ssh_data(b"hello world")
64
83
  signature.rewind()
65
- self.assertTrue(key.verify_ssh_sig(b"hello world", signature), "Signature is invalid")
84
+ self.assertTrue(
85
+ key.verify_ssh_sig(b"hello world", signature),
86
+ "Signature is invalid",
87
+ )
66
88
 
67
89
  @patch("azure.keyvault.keys.crypto.CryptographyClient")
68
90
  @patch("azure.keyvault.keys.KeyClient")
69
- def test_key_from_cloud_can_produce_valid_certificate(self, key_client_mock: Mock, crypto_client_mock: Mock):
91
+ def test_key_from_cloud_can_produce_valid_certificate(
92
+ self, key_client_mock: Mock, crypto_client_mock: Mock
93
+ ):
70
94
  from paramiko_cloud.azure.keys import ECDSAKey
95
+
71
96
  for curve, hash_ in self.ALL_SUPPORTED_ALGOS:
72
- with self.subTest("Using curve {} and hash {}".format(curve.name, hash_.name)):
73
- set_up_mocks(key_client_mock, crypto_client_mock, curve, hash_, self.TEST_KEY_NAME)
97
+ with self.subTest(
98
+ "Using curve {} and hash {}".format(curve.name, hash_.name)
99
+ ):
100
+ set_up_mocks(
101
+ key_client_mock,
102
+ crypto_client_mock,
103
+ curve,
104
+ hash_,
105
+ self.TEST_KEY_NAME,
106
+ )
74
107
  ca_key = ECDSAKey(None, None, "test_key")
75
108
  client_key = RSAKey.generate(1024)
76
- cert_string = ca_key.sign_certificate(client_key, ["test.user"]).cert_string()
109
+ cert_string = ca_key.sign_certificate(
110
+ client_key, ["test.user"]
111
+ ).cert_string()
77
112
  exit_code, cert_details = parse_certificate(cert_string)
78
113
  self.assertEqual(
79
114
  cert_details.public_key,
80
- "RSA-CERT SHA256:{}".format(sha256_fingerprint(client_key))
115
+ "RSA-CERT SHA256:{}".format(sha256_fingerprint(client_key)),
81
116
  )
82
117
  self.assertEqual(
83
118
  cert_details.signing_ca,
84
119
  "ECDSA SHA256:{} (using ecdsa-sha2-nistp{})".format(
85
- sha256_fingerprint(ca_key),
86
- ca_key.ecdsa_curve.key_length
87
- )
120
+ sha256_fingerprint(ca_key), ca_key.ecdsa_curve.key_length
121
+ ),
88
122
  )
89
123
  self.assertEqual(
90
- exit_code, 0,
124
+ exit_code,
125
+ 0,
91
126
  "Could not parse generated certificate with ssh-keygen, exit code {}".format(
92
127
  exit_code
93
- )
128
+ ),
94
129
  )
@@ -2,9 +2,14 @@ import abc
2
2
  import base64
3
3
  import hashlib
4
4
  from datetime import datetime
5
- from typing import Tuple, Optional, IO, Callable, Any
6
-
7
- from cryptography.hazmat.primitives.asymmetric.ec import ECDSA, EllipticCurvePublicKey, EllipticCurve
5
+ from typing import Any, Callable, IO, Optional, Tuple
6
+
7
+ from cryptography.hazmat.primitives.asymmetric.ec import (
8
+ ECDSA,
9
+ EllipticCurvePublicKey,
10
+ EllipticCurve,
11
+ )
12
+ from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
8
13
  from paramiko import ECDSAKey, Message
9
14
 
10
15
  from paramiko_cloud.pki import CertificateSigningKeyMixin
@@ -37,7 +42,10 @@ class CloudSigningKey(abc.ABC):
37
42
  Returns:
38
43
  The hash of the data
39
44
  """
40
- return getattr(hashlib, signature_algorithm.algorithm.name)(data).digest()
45
+ algorithm = signature_algorithm.algorithm
46
+ if isinstance(algorithm, Prehashed):
47
+ algorithm = algorithm._algorithm
48
+ return getattr(hashlib, algorithm.name)(data).digest()
41
49
 
42
50
  def sign(self, data: bytes, signature_algorithm: ECDSA) -> bytes:
43
51
  """
@@ -68,20 +76,28 @@ class BaseKeyECDSA(ECDSAKey, CertificateSigningKeyMixin):
68
76
  """
69
77
  super().__init__(vals=vals)
70
78
 
71
- def write_private_key_file(self, filename: str, password: Optional[str] = ...):
79
+ def write_private_key_file(
80
+ self, filename: str, password: Optional[str] = None
81
+ ) -> None:
72
82
  raise RuntimeError("Private key managed externally, cannot export")
73
83
 
74
- def write_private_key(self, file_obj: IO[str], password: Optional[str] = ...):
84
+ def write_private_key(
85
+ self, file_obj: IO[str], password: Optional[str] = None
86
+ ) -> None:
75
87
  raise RuntimeError("Private key managed externally, cannot export")
76
88
 
77
89
  @classmethod
78
90
  def generate(
79
- cls, curve: EllipticCurve = ..., progress_func: Optional[Callable[..., Any]] = ...,
80
- bits: Optional[int] = ...
81
- ):
82
- raise RuntimeError("Create new signing keys using the KMS client for your cloud provider")
91
+ cls,
92
+ curve: Optional[EllipticCurve] = None,
93
+ progress_func: Optional[Callable[..., Any]] = None,
94
+ bits: Optional[int] = None,
95
+ ) -> "BaseKeyECDSA":
96
+ raise RuntimeError(
97
+ "Create new signing keys using the KMS client for your cloud provider"
98
+ )
83
99
 
84
- def pubkey_string(self, comment=None) -> str:
100
+ def pubkey_string(self, comment: Optional[str] = None) -> str:
85
101
  """
86
102
  Render a string suitable for OpenSSH authorized_keys files
87
103
 
@@ -97,5 +113,5 @@ class BaseKeyECDSA(ECDSAKey, CertificateSigningKeyMixin):
97
113
  return "{key_type} {pubkey_string} {comment}".format(
98
114
  key_type=key_type,
99
115
  pubkey_string=base64.standard_b64encode(key_bytes).decode(),
100
- comment=comment or datetime.now().isoformat()
116
+ comment=comment or datetime.now().isoformat(),
101
117
  )
@@ -1,4 +1,4 @@
1
- from typing import Optional
1
+ from typing import Optional, cast
2
2
 
3
3
  from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey, ECDSA
4
4
  from cryptography.hazmat.primitives.serialization import load_pem_private_key
@@ -10,6 +10,7 @@ class _LocalSigningKey(CloudSigningKey):
10
10
  """
11
11
  A dummy signing key
12
12
  """
13
+
13
14
  def __init__(self, key: EllipticCurvePrivateKey):
14
15
  super().__init__(key.curve)
15
16
  self.key = key
@@ -26,7 +27,10 @@ class ECDSAKey(BaseKeyECDSA):
26
27
  pem_private_key: A PEM-formatted private key
27
28
  password: An optional password to decrypt the private key
28
29
  """
30
+
29
31
  def __init__(self, pem_private_key: bytes, password: Optional[bytes] = None):
30
- private_key: EllipticCurvePrivateKey = load_pem_private_key(pem_private_key, password)
32
+ private_key = cast(
33
+ EllipticCurvePrivateKey, load_pem_private_key(pem_private_key, password)
34
+ )
31
35
  public_key = private_key.public_key()
32
36
  super().__init__((_LocalSigningKey(private_key), public_key))