paramiko-cloud 1.3.1__tar.gz → 2.0.0__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.
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/PKG-INFO +5 -5
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/README.md +1 -1
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/aws/keys.py +20 -12
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/azure/keys.py +55 -31
- paramiko_cloud-2.0.0/paramiko_cloud/base.py +179 -0
- paramiko_cloud-2.0.0/paramiko_cloud/dummy/keys.py +47 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/gcp/keys.py +21 -13
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/grpc_server.py +6 -7
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/pki.py +92 -54
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/protobuf/csr_pb2.py +3 -3
- paramiko_cloud-2.0.0/paramiko_cloud/protobuf/csr_pb2.pyi +72 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/protobuf/rpc_pb2.py +3 -3
- paramiko_cloud-2.0.0/paramiko_cloud/protobuf/rpc_pb2.pyi +51 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/protobuf/rpc_pb2_grpc.py +4 -4
- paramiko_cloud-2.0.0/paramiko_cloud/py.typed +1 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud.egg-info/PKG-INFO +5 -5
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud.egg-info/SOURCES.txt +6 -8
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud.egg-info/requires.txt +2 -2
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/pyproject.toml +21 -14
- {paramiko_cloud-1.3.1/paramiko_cloud → paramiko_cloud-2.0.0/tests}/test_grpc_server.py +10 -10
- {paramiko_cloud-1.3.1/paramiko_cloud → paramiko_cloud-2.0.0/tests}/test_pki.py +24 -8
- paramiko_cloud-1.3.1/paramiko_cloud/aws/test_keys.py +0 -95
- paramiko_cloud-1.3.1/paramiko_cloud/azure/test_keys.py +0 -129
- paramiko_cloud-1.3.1/paramiko_cloud/base.py +0 -117
- paramiko_cloud-1.3.1/paramiko_cloud/dummy/keys.py +0 -36
- paramiko_cloud-1.3.1/paramiko_cloud/dummy/test_keys.py +0 -47
- paramiko_cloud-1.3.1/paramiko_cloud/gcp/test_keys.py +0 -95
- paramiko_cloud-1.3.1/paramiko_cloud/test_helpers.py +0 -87
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/aws/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/azure/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/dummy/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/gcp/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud/protobuf/__init__.py +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud.egg-info/dependency_links.txt +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/paramiko_cloud.egg-info/top_level.txt +0 -0
- {paramiko_cloud-1.3.1 → paramiko_cloud-2.0.0}/setup.cfg +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: paramiko-cloud
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Use cloud-managed keys to sign SSH certificates
|
|
5
5
|
Author-email: Jason Rigby <hello@jasonrig.by>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/jasonrig/paramiko-cloud/
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: paramiko
|
|
11
|
-
Requires-Dist: cryptography
|
|
10
|
+
Requires-Dist: paramiko>=5.0.0
|
|
11
|
+
Requires-Dist: cryptography>=45.0.1
|
|
12
12
|
Requires-Dist: protobuf
|
|
13
13
|
Requires-Dist: grpcio-tools
|
|
14
14
|
Provides-Extra: aws
|
|
@@ -55,5 +55,5 @@ uv run pytest --cov=./ --cov-report=xml
|
|
|
55
55
|
uv run python scripts/build_proto.py
|
|
56
56
|
uv run ruff check .
|
|
57
57
|
uv run ruff format . --check
|
|
58
|
-
uv run mypy paramiko_cloud
|
|
58
|
+
uv run mypy paramiko_cloud tests
|
|
59
59
|
```
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
import boto3
|
|
4
4
|
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
5
|
-
ECDSA,
|
|
6
|
-
EllipticCurve,
|
|
7
5
|
EllipticCurvePublicKey,
|
|
6
|
+
EllipticCurveSignatureAlgorithm,
|
|
8
7
|
)
|
|
9
8
|
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
|
9
|
+
from cryptography.utils import Buffer
|
|
10
10
|
|
|
11
11
|
from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey
|
|
12
12
|
|
|
@@ -17,7 +17,11 @@ class _AWSSigningKey(CloudSigningKey):
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
20
|
-
self,
|
|
20
|
+
self,
|
|
21
|
+
client: Any,
|
|
22
|
+
key_id: str,
|
|
23
|
+
signing_algo: str,
|
|
24
|
+
public_key: EllipticCurvePublicKey,
|
|
21
25
|
) -> None:
|
|
22
26
|
"""
|
|
23
27
|
Constructor
|
|
@@ -26,15 +30,19 @@ class _AWSSigningKey(CloudSigningKey):
|
|
|
26
30
|
client: the AWS KMS client from boto3
|
|
27
31
|
key_id: the AWS KMS key id
|
|
28
32
|
signing_algo: the signing algorithm to use
|
|
29
|
-
|
|
33
|
+
public_key: the public key corresponding to the KMS key
|
|
30
34
|
"""
|
|
31
35
|
|
|
32
|
-
super().__init__(
|
|
36
|
+
super().__init__(public_key)
|
|
33
37
|
self.client = client
|
|
34
38
|
self.key_id = key_id
|
|
35
39
|
self.signing_algo = signing_algo
|
|
36
40
|
|
|
37
|
-
def sign(
|
|
41
|
+
def sign(
|
|
42
|
+
self,
|
|
43
|
+
data: Buffer,
|
|
44
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
45
|
+
) -> bytes:
|
|
38
46
|
"""
|
|
39
47
|
Calculate the signature for the given data
|
|
40
48
|
|
|
@@ -93,9 +101,9 @@ class ECDSAKey(BaseKeyECDSA):
|
|
|
93
101
|
), "No supported key/algorithm pair found."
|
|
94
102
|
assert pub_key["KeyUsage"] == "SIGN_VERIFY", "Key does not support signing."
|
|
95
103
|
|
|
96
|
-
verifying_key =
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
verifying_key = load_der_public_key(pub_key["PublicKey"])
|
|
105
|
+
if not isinstance(verifying_key, EllipticCurvePublicKey):
|
|
106
|
+
raise TypeError("AWS KMS public key is not an elliptic curve key")
|
|
99
107
|
|
|
100
108
|
super().__init__(
|
|
101
109
|
(
|
|
@@ -103,13 +111,13 @@ class ECDSAKey(BaseKeyECDSA):
|
|
|
103
111
|
client,
|
|
104
112
|
key_id,
|
|
105
113
|
self._choose_signing_algo(set(pub_key["SigningAlgorithms"])),
|
|
106
|
-
verifying_key
|
|
114
|
+
verifying_key,
|
|
107
115
|
),
|
|
108
116
|
verifying_key,
|
|
109
117
|
)
|
|
110
118
|
)
|
|
111
119
|
|
|
112
|
-
def _choose_signing_algo(self, supported_algos:
|
|
120
|
+
def _choose_signing_algo(self, supported_algos: set[str]) -> str:
|
|
113
121
|
"""
|
|
114
122
|
Selects the appropriate signing algorithm based on the supported and offered signing algorithms
|
|
115
123
|
|
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
from
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Protocol, runtime_checkable
|
|
2
3
|
|
|
3
4
|
from azure.identity import (
|
|
4
|
-
|
|
5
|
+
AzureCliCredential,
|
|
5
6
|
AzurePowerShellCredential,
|
|
6
|
-
InteractiveBrowserCredential,
|
|
7
7
|
ChainedTokenCredential,
|
|
8
|
+
DefaultAzureCredential,
|
|
8
9
|
EnvironmentCredential,
|
|
10
|
+
InteractiveBrowserCredential,
|
|
9
11
|
ManagedIdentityCredential,
|
|
10
12
|
SharedTokenCacheCredential,
|
|
11
|
-
AzureCliCredential,
|
|
12
13
|
VisualStudioCodeCredential,
|
|
13
14
|
)
|
|
14
15
|
from azure.keyvault.keys import KeyClient
|
|
15
16
|
from azure.keyvault.keys.crypto import CryptographyClient, SignatureAlgorithm
|
|
16
17
|
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
SECP192R1,
|
|
19
|
+
SECP224R1,
|
|
19
20
|
SECP256R1,
|
|
20
21
|
SECP384R1,
|
|
21
22
|
SECP521R1,
|
|
22
|
-
SECP224R1,
|
|
23
|
-
SECP192R1,
|
|
24
23
|
EllipticCurve,
|
|
24
|
+
EllipticCurvePublicKey,
|
|
25
|
+
EllipticCurvePublicNumbers,
|
|
26
|
+
EllipticCurveSignatureAlgorithm,
|
|
25
27
|
)
|
|
26
28
|
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
|
|
29
|
+
from cryptography.utils import Buffer
|
|
27
30
|
|
|
28
31
|
from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey
|
|
29
32
|
|
|
30
|
-
_CURVES = {
|
|
33
|
+
_CURVES: dict[str, Callable[[], EllipticCurve]] = {
|
|
31
34
|
"P-256": SECP256R1,
|
|
32
35
|
"P-384": SECP384R1,
|
|
33
36
|
"P-521": SECP521R1,
|
|
@@ -36,17 +39,30 @@ _CURVES = {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class _ECJsonWebKey(Protocol):
|
|
44
|
+
"""Elliptic-curve fields dynamically provided by Azure's JsonWebKey."""
|
|
45
|
+
|
|
46
|
+
crv: str
|
|
47
|
+
x: bytes
|
|
48
|
+
y: bytes
|
|
49
|
+
|
|
50
|
+
|
|
39
51
|
class _AzureSigningKey(CloudSigningKey):
|
|
40
52
|
"""
|
|
41
53
|
Provides signing operations to Paramiko for the Azure Key Vault-backed key
|
|
42
54
|
|
|
43
55
|
Args:
|
|
44
56
|
crypto_client: the Key Vault Cryptography Client authenticated to access the selected key
|
|
45
|
-
|
|
57
|
+
public_key: the public key corresponding to the Key Vault key
|
|
46
58
|
"""
|
|
47
59
|
|
|
48
|
-
def __init__(
|
|
49
|
-
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
crypto_client: CryptographyClient,
|
|
63
|
+
public_key: EllipticCurvePublicKey,
|
|
64
|
+
):
|
|
65
|
+
super().__init__(public_key)
|
|
50
66
|
self.crypto_client = crypto_client
|
|
51
67
|
|
|
52
68
|
def _signaure_algorithm(self) -> SignatureAlgorithm:
|
|
@@ -66,7 +82,11 @@ class _AzureSigningKey(CloudSigningKey):
|
|
|
66
82
|
else:
|
|
67
83
|
raise NotImplementedError("Unsupported EC signature algorithm")
|
|
68
84
|
|
|
69
|
-
def sign(
|
|
85
|
+
def sign(
|
|
86
|
+
self,
|
|
87
|
+
data: Buffer,
|
|
88
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
89
|
+
) -> bytes:
|
|
70
90
|
"""
|
|
71
91
|
Calculate the signature for the given data
|
|
72
92
|
|
|
@@ -103,44 +123,48 @@ class ECDSAKey(BaseKeyECDSA):
|
|
|
103
123
|
|
|
104
124
|
def __init__(
|
|
105
125
|
self,
|
|
106
|
-
credential:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
VisualStudioCodeCredential,
|
|
116
|
-
],
|
|
126
|
+
credential: DefaultAzureCredential
|
|
127
|
+
| AzurePowerShellCredential
|
|
128
|
+
| InteractiveBrowserCredential
|
|
129
|
+
| ChainedTokenCredential
|
|
130
|
+
| EnvironmentCredential
|
|
131
|
+
| ManagedIdentityCredential
|
|
132
|
+
| SharedTokenCacheCredential
|
|
133
|
+
| AzureCliCredential
|
|
134
|
+
| VisualStudioCodeCredential,
|
|
117
135
|
vault_url: str,
|
|
118
136
|
key_name: str,
|
|
119
137
|
):
|
|
120
138
|
vault_client = KeyClient(vault_url, credential=credential)
|
|
121
139
|
pub_key = vault_client.get_key(key_name)
|
|
122
140
|
assert pub_key.key_type in self._ALLOWED_ALGOS, (
|
|
123
|
-
"Unsupported signing algorithm: {
|
|
141
|
+
f"Unsupported signing algorithm: {pub_key.key_type}"
|
|
124
142
|
)
|
|
125
143
|
|
|
126
144
|
jwk = pub_key.key
|
|
127
145
|
assert jwk is not None, "Missing key material from Azure Key Vault."
|
|
128
|
-
|
|
146
|
+
if not isinstance(jwk, _ECJsonWebKey):
|
|
147
|
+
raise TypeError("Azure Key Vault returned incomplete EC key material")
|
|
148
|
+
if not isinstance(jwk.crv, str):
|
|
149
|
+
raise TypeError("Azure Key Vault returned an invalid EC curve name")
|
|
150
|
+
if not isinstance(jwk.x, bytes) or not isinstance(jwk.y, bytes):
|
|
151
|
+
raise TypeError("Azure Key Vault returned invalid EC coordinates")
|
|
152
|
+
curve_name = jwk.crv
|
|
129
153
|
|
|
130
|
-
assert curve_name in _CURVES, "Unsupported curve: {}"
|
|
154
|
+
assert curve_name in _CURVES, f"Unsupported curve: {curve_name}"
|
|
131
155
|
|
|
132
|
-
curve = _CURVES[curve_name]()
|
|
156
|
+
curve = _CURVES[curve_name]()
|
|
133
157
|
|
|
134
158
|
verifying_key = EllipticCurvePublicNumbers(
|
|
135
|
-
int.from_bytes(
|
|
136
|
-
int.from_bytes(
|
|
159
|
+
int.from_bytes(jwk.x, "big"),
|
|
160
|
+
int.from_bytes(jwk.y, "big"),
|
|
137
161
|
curve,
|
|
138
162
|
).public_key()
|
|
139
163
|
|
|
140
164
|
super().__init__(
|
|
141
165
|
(
|
|
142
166
|
_AzureSigningKey(
|
|
143
|
-
CryptographyClient(pub_key, credential), verifying_key
|
|
167
|
+
CryptographyClient(pub_key, credential), verifying_key
|
|
144
168
|
),
|
|
145
169
|
verifying_key,
|
|
146
170
|
)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any, Protocol
|
|
8
|
+
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
10
|
+
ECDH,
|
|
11
|
+
ECDSA,
|
|
12
|
+
EllipticCurve,
|
|
13
|
+
EllipticCurvePrivateKey,
|
|
14
|
+
EllipticCurvePrivateNumbers,
|
|
15
|
+
EllipticCurvePublicKey,
|
|
16
|
+
EllipticCurveSignatureAlgorithm,
|
|
17
|
+
)
|
|
18
|
+
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
|
19
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
20
|
+
Encoding,
|
|
21
|
+
KeySerializationEncryption,
|
|
22
|
+
PrivateFormat,
|
|
23
|
+
)
|
|
24
|
+
from cryptography.utils import Buffer
|
|
25
|
+
from paramiko import ECDSAKey, Message
|
|
26
|
+
from paramiko.pkey import PEM, FileFormat
|
|
27
|
+
|
|
28
|
+
from paramiko_cloud.pki import CertificateSigningKeyMixin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _SupportsWrite(Protocol):
|
|
32
|
+
def write(self, data: str) -> object: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CloudSigningKey(EllipticCurvePrivateKey):
|
|
36
|
+
"""
|
|
37
|
+
Base class for all cloud KMS-backed signing keys
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, public_key: EllipticCurvePublicKey):
|
|
41
|
+
"""
|
|
42
|
+
Constructor
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
public_key: the public key corresponding to the cloud-managed key
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
self._public_key = public_key
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def curve(self) -> EllipticCurve:
|
|
52
|
+
return self._public_key.curve
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def key_size(self) -> int:
|
|
56
|
+
return self.curve.key_size
|
|
57
|
+
|
|
58
|
+
def public_key(self) -> EllipticCurvePublicKey:
|
|
59
|
+
return self._public_key
|
|
60
|
+
|
|
61
|
+
def __copy__(self) -> "CloudSigningKey":
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __deepcopy__(self, memo: dict[object, object]) -> "CloudSigningKey":
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def exchange(
|
|
68
|
+
self, algorithm: ECDH, peer_public_key: EllipticCurvePublicKey
|
|
69
|
+
) -> bytes:
|
|
70
|
+
raise RuntimeError("Key exchange is unavailable for cloud-managed keys")
|
|
71
|
+
|
|
72
|
+
def private_numbers(self) -> EllipticCurvePrivateNumbers:
|
|
73
|
+
raise RuntimeError("Private key material is managed externally")
|
|
74
|
+
|
|
75
|
+
def private_bytes(
|
|
76
|
+
self,
|
|
77
|
+
encoding: Encoding,
|
|
78
|
+
format: PrivateFormat,
|
|
79
|
+
encryption_algorithm: KeySerializationEncryption,
|
|
80
|
+
) -> bytes:
|
|
81
|
+
raise RuntimeError("Private key material is managed externally")
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def digest(
|
|
85
|
+
data: Buffer,
|
|
86
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
87
|
+
) -> bytes:
|
|
88
|
+
"""
|
|
89
|
+
Calculates the hash of the given data according to the given elliptic curve key
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: the data for which to calculate the hash
|
|
93
|
+
signature_algorithm: the elliptic curve signature algorithm
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The hash of the data
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(signature_algorithm, ECDSA):
|
|
99
|
+
raise TypeError("Cloud-managed keys require an ECDSA signature algorithm")
|
|
100
|
+
algorithm = signature_algorithm.algorithm
|
|
101
|
+
if isinstance(algorithm, Prehashed):
|
|
102
|
+
algorithm = algorithm._algorithm
|
|
103
|
+
return getattr(hashlib, algorithm.name)(data).digest()
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def sign(
|
|
107
|
+
self,
|
|
108
|
+
data: Buffer,
|
|
109
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
110
|
+
) -> bytes:
|
|
111
|
+
"""
|
|
112
|
+
Calculate the signature for the given data
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
data: data for which to calculate a signature
|
|
116
|
+
signature_algorithm: the curve used for this signature
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The DER formatted signature
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class BaseKeyECDSA(ECDSAKey, CertificateSigningKeyMixin):
|
|
126
|
+
"""
|
|
127
|
+
Base class for all cloud-backed ECDSA keys
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, vals: tuple[CloudSigningKey, EllipticCurvePublicKey]):
|
|
131
|
+
"""
|
|
132
|
+
Constructor
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
vals: tuple of signing key and verifying key
|
|
136
|
+
"""
|
|
137
|
+
super().__init__(vals=vals)
|
|
138
|
+
|
|
139
|
+
def write_private_key_file(
|
|
140
|
+
self,
|
|
141
|
+
filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
|
|
142
|
+
password: str | None = None,
|
|
143
|
+
file_format: FileFormat = PEM,
|
|
144
|
+
) -> None:
|
|
145
|
+
raise RuntimeError("Private key managed externally, cannot export")
|
|
146
|
+
|
|
147
|
+
def write_private_key(
|
|
148
|
+
self,
|
|
149
|
+
file_obj: _SupportsWrite,
|
|
150
|
+
password: str | None = None,
|
|
151
|
+
file_format: FileFormat = PEM,
|
|
152
|
+
) -> None:
|
|
153
|
+
raise RuntimeError("Private key managed externally, cannot export")
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def generate(
|
|
157
|
+
cls,
|
|
158
|
+
curve: EllipticCurve | None = None,
|
|
159
|
+
progress_func: Callable[..., Any] | None = None,
|
|
160
|
+
bits: int | None = None,
|
|
161
|
+
) -> "BaseKeyECDSA":
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
"Create new signing keys using the KMS client for your cloud provider"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def pubkey_string(self, comment: str | None = None) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Render a string suitable for OpenSSH authorized_keys files
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
comment: an optional comment, defaulting to the current date and time in ISO format
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
The public key string
|
|
175
|
+
"""
|
|
176
|
+
key_bytes = self.asbytes()
|
|
177
|
+
m = Message(self.asbytes())
|
|
178
|
+
key_type = m.get_text()
|
|
179
|
+
return f"{key_type} {base64.standard_b64encode(key_bytes).decode()} {comment or datetime.now(timezone.utc).isoformat()}"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
2
|
+
EllipticCurvePrivateKey,
|
|
3
|
+
EllipticCurvePublicKey,
|
|
4
|
+
EllipticCurveSignatureAlgorithm,
|
|
5
|
+
)
|
|
6
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
7
|
+
from cryptography.utils import Buffer
|
|
8
|
+
|
|
9
|
+
from paramiko_cloud.base import BaseKeyECDSA, CloudSigningKey
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _LocalSigningKey(CloudSigningKey):
|
|
13
|
+
"""
|
|
14
|
+
A dummy signing key
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
key: EllipticCurvePrivateKey,
|
|
20
|
+
public_key: EllipticCurvePublicKey,
|
|
21
|
+
):
|
|
22
|
+
super().__init__(public_key)
|
|
23
|
+
self.key = key
|
|
24
|
+
|
|
25
|
+
def sign(
|
|
26
|
+
self,
|
|
27
|
+
data: Buffer,
|
|
28
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
29
|
+
) -> bytes:
|
|
30
|
+
return self.key.sign(data, signature_algorithm)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ECDSAKey(BaseKeyECDSA):
|
|
34
|
+
"""
|
|
35
|
+
A dummy key that demonstrates the abstraction, but just loads they key from file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
pem_private_key: A PEM-formatted private key
|
|
39
|
+
password: An optional password to decrypt the private key
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, pem_private_key: bytes, password: bytes | None = None):
|
|
43
|
+
private_key = load_pem_private_key(pem_private_key, password)
|
|
44
|
+
if not isinstance(private_key, EllipticCurvePrivateKey):
|
|
45
|
+
raise TypeError("PEM private key is not an elliptic curve key")
|
|
46
|
+
public_key = private_key.public_key()
|
|
47
|
+
super().__init__((_LocalSigningKey(private_key, public_key), public_key))
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from cryptography.hazmat.primitives.asymmetric.ec import (
|
|
2
|
+
ECDSA,
|
|
3
|
+
EllipticCurvePublicKey,
|
|
4
|
+
EllipticCurveSignatureAlgorithm,
|
|
5
|
+
)
|
|
4
6
|
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
|
|
5
7
|
from cryptography.hazmat.primitives.hashes import HashAlgorithm
|
|
6
|
-
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
|
|
7
8
|
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
9
|
+
from cryptography.utils import Buffer
|
|
8
10
|
from google.cloud import kms
|
|
9
11
|
from google.cloud.kms_v1 import Digest
|
|
10
12
|
|
|
@@ -17,20 +19,24 @@ class _GCPSigningKey(CloudSigningKey):
|
|
|
17
19
|
Args:
|
|
18
20
|
kms_client: a KMS client that can access the selected key
|
|
19
21
|
key_name: the name of the key
|
|
20
|
-
|
|
22
|
+
public_key: the public key corresponding to the KMS key
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
def __init__(
|
|
24
26
|
self,
|
|
25
27
|
kms_client: kms.KeyManagementServiceClient,
|
|
26
28
|
key_name: str,
|
|
27
|
-
|
|
29
|
+
public_key: EllipticCurvePublicKey,
|
|
28
30
|
):
|
|
29
|
-
super().__init__(
|
|
31
|
+
super().__init__(public_key)
|
|
30
32
|
self.client = kms_client
|
|
31
33
|
self.key_name = key_name
|
|
32
34
|
|
|
33
|
-
def sign(
|
|
35
|
+
def sign(
|
|
36
|
+
self,
|
|
37
|
+
data: Buffer,
|
|
38
|
+
signature_algorithm: EllipticCurveSignatureAlgorithm,
|
|
39
|
+
) -> bytes:
|
|
34
40
|
"""
|
|
35
41
|
Calculate the signature for the given data
|
|
36
42
|
|
|
@@ -41,6 +47,8 @@ class _GCPSigningKey(CloudSigningKey):
|
|
|
41
47
|
Returns:
|
|
42
48
|
The DER formatted signature
|
|
43
49
|
"""
|
|
50
|
+
if not isinstance(signature_algorithm, ECDSA):
|
|
51
|
+
raise TypeError("GCP KMS keys require an ECDSA signature algorithm")
|
|
44
52
|
if isinstance(signature_algorithm.algorithm, Prehashed):
|
|
45
53
|
algorithm: HashAlgorithm = signature_algorithm.algorithm._algorithm
|
|
46
54
|
else:
|
|
@@ -74,14 +82,14 @@ class ECDSAKey(BaseKeyECDSA):
|
|
|
74
82
|
def __init__(self, kms_client: kms.KeyManagementServiceClient, key_name: str):
|
|
75
83
|
pub_key = kms_client.get_public_key(name=key_name)
|
|
76
84
|
assert pub_key.algorithm.name in self._ALLOWED_ALGOS, (
|
|
77
|
-
"Unsupported signing algorithm: {
|
|
78
|
-
)
|
|
79
|
-
verifying_key = cast(
|
|
80
|
-
EllipticCurvePublicKey, load_pem_public_key(pub_key.pem.encode())
|
|
85
|
+
f"Unsupported signing algorithm: {pub_key.algorithm.name}"
|
|
81
86
|
)
|
|
87
|
+
verifying_key = load_pem_public_key(pub_key.pem.encode())
|
|
88
|
+
if not isinstance(verifying_key, EllipticCurvePublicKey):
|
|
89
|
+
raise TypeError("GCP KMS public key is not an elliptic curve key")
|
|
82
90
|
super().__init__(
|
|
83
91
|
(
|
|
84
|
-
_GCPSigningKey(kms_client, pub_key.name, verifying_key
|
|
92
|
+
_GCPSigningKey(kms_client, pub_key.name, verifying_key),
|
|
85
93
|
verifying_key,
|
|
86
94
|
)
|
|
87
95
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from concurrent import futures
|
|
2
2
|
from types import TracebackType
|
|
3
|
-
from typing import Optional, Type
|
|
4
3
|
|
|
5
4
|
import grpc
|
|
6
5
|
|
|
@@ -25,14 +24,14 @@ class GRPCServer:
|
|
|
25
24
|
signer_servicer: SignerServicer,
|
|
26
25
|
bind_addr: str = "[::]",
|
|
27
26
|
port: int = 50051,
|
|
28
|
-
server_credentials:
|
|
27
|
+
server_credentials: grpc.ServerCredentials | None = None,
|
|
29
28
|
max_workers: int = 10,
|
|
30
|
-
shutdown_grace:
|
|
29
|
+
shutdown_grace: int | None = None,
|
|
31
30
|
):
|
|
32
31
|
self.shutdown_grace = shutdown_grace
|
|
33
32
|
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_workers))
|
|
34
33
|
rpc_pb2_grpc.add_SignerServicer_to_server(signer_servicer, self.server)
|
|
35
|
-
address = "{}:{}"
|
|
34
|
+
address = f"{bind_addr}:{port}"
|
|
36
35
|
if server_credentials:
|
|
37
36
|
self.server.add_secure_port(address, server_credentials)
|
|
38
37
|
else:
|
|
@@ -44,8 +43,8 @@ class GRPCServer:
|
|
|
44
43
|
|
|
45
44
|
def __exit__(
|
|
46
45
|
self,
|
|
47
|
-
exc_type:
|
|
48
|
-
exc_val:
|
|
49
|
-
exc_tb:
|
|
46
|
+
exc_type: type[BaseException] | None,
|
|
47
|
+
exc_val: BaseException | None,
|
|
48
|
+
exc_tb: TracebackType | None,
|
|
50
49
|
) -> None:
|
|
51
50
|
self.server.stop(self.shutdown_grace)
|