Skip to content

Commit e479720

Browse files
reaperhulkalex
andauthored
document how to use alternate backends with our asymmetric abcs (#12881)
* document how to use alternate backends with our asymmetric abcs * spellchecker is a boorish oaf that doesn't know latin * Apply suggestions from code review Co-authored-by: Alex Gaynor <[email protected]> * review feedback --------- Co-authored-by: Alex Gaynor <[email protected]>
1 parent b0e3a55 commit e479720

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
.. hazmat::
2+
3+
Cloud KMS and HSM Asymmetric Keys
4+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5+
6+
.. testsetup::
7+
8+
"""
9+
We need to have this exist so the doctest below allows us to
10+
test that we're satisfying the base class requirements.
11+
"""
12+
class Response:
13+
def __init__(self, signature):
14+
self.signature = signature
15+
16+
class SomeCloudClient:
17+
def __init__(self, creds):
18+
pass
19+
20+
def sign(self, key_id, algorithm, message):
21+
return Response(b"\x00" * (self.key_size(key_id) // 8))
22+
23+
def key_size(self, key_id):
24+
return 2048
25+
26+
``cryptography`` provides a set of abstract base classes for asymmetric keys
27+
that can be used to integrate with cloud key management services, HSMs, and other ways of managing keys that are not in-memory.
28+
A minimal example with a hypothetical cloud key management service for an RSA
29+
key is provided below, but this works for all asymmetric types. You must provide
30+
all methods of the base class, but many methods can be stubs with no implementation
31+
if you only need a subset of functionality.
32+
33+
.. doctest::
34+
35+
>>> import typing
36+
>>> from cryptography.hazmat.primitives.asymmetric import rsa, utils
37+
>>> from cryptography.hazmat.primitives import hashes, serialization
38+
>>> from cryptography.hazmat.primitives.asymmetric.padding import AsymmetricPadding, PKCS1v15
39+
>>>
40+
>>> class CloudRSAPrivateKey(rsa.RSAPrivateKey):
41+
... def __init__(self, creds, key_id):
42+
... self._creds = creds
43+
... self._cloud_client = SomeCloudClient(creds)
44+
... self._key_id = key_id
45+
...
46+
... def sign(
47+
... self,
48+
... data: bytes,
49+
... padding: AsymmetricPadding,
50+
... algorithm: typing.Union[utils.Prehashed, hashes.HashAlgorithm],
51+
... ) -> bytes:
52+
... """
53+
... Signs data using the cloud KMS. You'll need to define a mapping
54+
... between the way your cloud provider represents padding and algorithms
55+
... and the way cryptography represents them.
56+
... """
57+
...
58+
... # Hash the data if necessary
59+
... if not isinstance(algorithm, utils.Prehashed):
60+
... h = hashes.Hash(algorithm)
61+
... h.update(data)
62+
... digest = h.finalize()
63+
... hash_alg = algorithm
64+
... else:
65+
... digest = data
66+
... hash_alg = algorithm._algorithm
67+
... # Map cryptography padding/algorithm to KMS signing algorithm
68+
... kms_algorithm = self._map_to_kms_algorithm(padding, hash_alg)
69+
...
70+
... # Call KMS API to sign the digest
71+
... response = self._cloud_client.sign(
72+
... key_id=self._key_id,
73+
... algorithm=kms_algorithm,
74+
... message=digest,
75+
... )
76+
...
77+
... return response.signature
78+
...
79+
... def decrypt(self, ciphertext: bytes, padding: AsymmetricPadding) -> bytes:
80+
... raise NotImplementedError()
81+
...
82+
... def _map_to_kms_algorithm(
83+
... self,
84+
... padding: AsymmetricPadding,
85+
... algorithm: hashes.HashAlgorithm
86+
... ) -> bytes:
87+
... """
88+
... Maps the cryptography padding and algorithm to the corresponding KMS signing algorithm.
89+
... This is specific to your implementation.
90+
... """
91+
... if isinstance(padding, PKCS1v15) and isinstance(algorithm, hashes.SHA256):
92+
... return b"RSA_PKCS1_V1_5_SHA_256"
93+
... else:
94+
... raise NotImplementedError()
95+
...
96+
... @property
97+
... def key_size(self) -> int:
98+
... return self._cloud_client.key_size(self._key_id)
99+
...
100+
... def public_key(self) -> rsa.RSAPublicKey:
101+
... raise NotImplementedError()
102+
...
103+
... def private_numbers(self) -> rsa.RSAPrivateNumbers:
104+
... """
105+
... This method typically can't be implemented for cloud KMS keys
106+
... as the private key material is not accessible.
107+
... """
108+
... raise NotImplementedError()
109+
...
110+
... def private_bytes(
111+
... self,
112+
... encoding: serialization.Encoding,
113+
... format: serialization.PrivateFormat,
114+
... encryption_algorithm: serialization.KeySerializationEncryption,
115+
... ) -> bytes:
116+
... """
117+
... This method typically can't be implemented for cloud KMS keys
118+
... as the private key material is not accessible.
119+
... """
120+
... raise NotImplementedError()
121+
...
122+
... def __copy__(self) -> "CloudRSAPrivateKey":
123+
... return self
124+
...
125+
>>> cloud_private_key = CloudRSAPrivateKey("creds", "key_id")
126+
>>> sig = cloud_private_key.sign(b"message", PKCS1v15(), hashes.SHA256())
127+
>>> isinstance(sig, bytes)
128+
True
129+
130+
This key can then be used with other parts of ``cryptography``, such as the X.509 APIs.
131+
In the example below we assume that we are using our cloud private key to sign
132+
a leaf certificate (not self-signed).
133+
134+
.. doctest::
135+
136+
>>> from cryptography import x509
137+
>>> from cryptography.x509.oid import NameOID
138+
>>> import datetime
139+
>>> one_day = datetime.timedelta(1, 0, 0)
140+
>>> leaf_private_key = rsa.generate_private_key(
141+
... public_exponent=65537,
142+
... key_size=2048,
143+
... )
144+
>>> leaf_public_key = leaf_private_key.public_key()
145+
>>> builder = x509.CertificateBuilder()
146+
>>> builder = builder.subject_name(x509.Name([
147+
... x509.NameAttribute(NameOID.COMMON_NAME, 'cryptography.io'),
148+
... ]))
149+
>>> builder = builder.issuer_name(x509.Name([
150+
... x509.NameAttribute(NameOID.COMMON_NAME, 'My Cloud CA'),
151+
... ]))
152+
>>> builder = builder.not_valid_before(datetime.datetime.today() - one_day)
153+
>>> builder = builder.not_valid_after(datetime.datetime.today() + (one_day * 30))
154+
>>> builder = builder.serial_number(x509.random_serial_number())
155+
>>> builder = builder.public_key(leaf_public_key)
156+
>>> builder = builder.add_extension(
157+
... x509.SubjectAlternativeName(
158+
... [x509.DNSName('cryptography.io')]
159+
... ),
160+
... critical=False
161+
... )
162+
>>> builder = builder.add_extension(
163+
... x509.BasicConstraints(ca=False, path_length=None), critical=True,
164+
... )
165+
>>> certificate = builder.sign(
166+
... private_key=cloud_private_key, algorithm=hashes.SHA256(),
167+
... )
168+
>>> isinstance(certificate, x509.Certificate)
169+
True

docs/hazmat/primitives/asymmetric/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ private key is able to decrypt it.
3333
dsa
3434
serialization
3535
utils
36+
cloudhsm
3637

3738

3839
.. _`proof of identity`: https://en.wikipedia.org/wiki/Public-key_infrastructure

0 commit comments

Comments
 (0)