mongo-charms-single-kernel 1.8.7__py3-none-any.whl → 1.8.9__py3-none-any.whl
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.
Potentially problematic release.
This version of mongo-charms-single-kernel might be problematic. Click here for more details.
- {mongo_charms_single_kernel-1.8.7.dist-info → mongo_charms_single_kernel-1.8.9.dist-info}/METADATA +1 -1
- {mongo_charms_single_kernel-1.8.7.dist-info → mongo_charms_single_kernel-1.8.9.dist-info}/RECORD +30 -28
- single_kernel_mongo/config/literals.py +8 -3
- single_kernel_mongo/config/models.py +12 -0
- single_kernel_mongo/config/relations.py +2 -1
- single_kernel_mongo/config/statuses.py +127 -20
- single_kernel_mongo/core/operator.py +68 -1
- single_kernel_mongo/core/structured_config.py +2 -0
- single_kernel_mongo/core/workload.py +10 -4
- single_kernel_mongo/events/cluster.py +5 -0
- single_kernel_mongo/events/sharding.py +3 -1
- single_kernel_mongo/events/tls.py +183 -157
- single_kernel_mongo/exceptions.py +0 -8
- single_kernel_mongo/lib/charms/operator_libs_linux/v1/systemd.py +288 -0
- single_kernel_mongo/lib/charms/tls_certificates_interface/v4/tls_certificates.py +1995 -0
- single_kernel_mongo/managers/cluster.py +70 -28
- single_kernel_mongo/managers/config.py +14 -8
- single_kernel_mongo/managers/mongo.py +1 -1
- single_kernel_mongo/managers/mongodb_operator.py +53 -56
- single_kernel_mongo/managers/mongos_operator.py +18 -20
- single_kernel_mongo/managers/sharding.py +154 -127
- single_kernel_mongo/managers/tls.py +223 -206
- single_kernel_mongo/state/charm_state.py +39 -16
- single_kernel_mongo/state/cluster_state.py +8 -0
- single_kernel_mongo/state/config_server_state.py +9 -0
- single_kernel_mongo/state/tls_state.py +39 -12
- single_kernel_mongo/templates/enable-transparent-huge-pages.service.j2 +14 -0
- single_kernel_mongo/utils/helpers.py +4 -19
- single_kernel_mongo/lib/charms/tls_certificates_interface/v3/tls_certificates.py +0 -2123
- {mongo_charms_single_kernel-1.8.7.dist-info → mongo_charms_single_kernel-1.8.9.dist-info}/WHEEL +0 -0
- {mongo_charms_single_kernel-1.8.7.dist-info → mongo_charms_single_kernel-1.8.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,2123 +0,0 @@
|
|
|
1
|
-
# Copyright 2024 Canonical Ltd.
|
|
2
|
-
# See LICENSE file for licensing details.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"""Library for the tls-certificates relation.
|
|
6
|
-
|
|
7
|
-
This library contains the Requires and Provides classes for handling the tls-certificates
|
|
8
|
-
interface.
|
|
9
|
-
|
|
10
|
-
Pre-requisites:
|
|
11
|
-
- Juju >= 3.0
|
|
12
|
-
|
|
13
|
-
## Getting Started
|
|
14
|
-
From a charm directory, fetch the library using `charmcraft`:
|
|
15
|
-
|
|
16
|
-
```shell
|
|
17
|
-
charmcraft fetch-lib charms.tls_certificates_interface.v3.tls_certificates
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Add the following libraries to the charm's `requirements.txt` file:
|
|
21
|
-
- jsonschema
|
|
22
|
-
- cryptography >= 42.0.0
|
|
23
|
-
|
|
24
|
-
Add the following section to the charm's `charmcraft.yaml` file:
|
|
25
|
-
```yaml
|
|
26
|
-
parts:
|
|
27
|
-
charm:
|
|
28
|
-
build-packages:
|
|
29
|
-
- libffi-dev
|
|
30
|
-
- libssl-dev
|
|
31
|
-
- rustc
|
|
32
|
-
- cargo
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Provider charm
|
|
36
|
-
The provider charm is the charm providing certificates to another charm that requires them. In
|
|
37
|
-
this example, the provider charm is storing its private key using a peer relation interface called
|
|
38
|
-
`replicas`.
|
|
39
|
-
|
|
40
|
-
Example:
|
|
41
|
-
```python
|
|
42
|
-
from charms.tls_certificates_interface.v3.tls_certificates import (
|
|
43
|
-
CertificateCreationRequestEvent,
|
|
44
|
-
CertificateRevocationRequestEvent,
|
|
45
|
-
TLSCertificatesProvidesV3,
|
|
46
|
-
generate_private_key,
|
|
47
|
-
)
|
|
48
|
-
from ops.charm import CharmBase, InstallEvent
|
|
49
|
-
from ops.main import main
|
|
50
|
-
from ops.model import ActiveStatus, WaitingStatus
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def generate_ca(private_key: bytes, subject: str) -> str:
|
|
54
|
-
return "whatever ca content"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def generate_certificate(ca: str, private_key: str, csr: str) -> str:
|
|
58
|
-
return "Whatever certificate"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class ExampleProviderCharm(CharmBase):
|
|
62
|
-
|
|
63
|
-
def __init__(self, *args):
|
|
64
|
-
super().__init__(*args)
|
|
65
|
-
self.certificates = TLSCertificatesProvidesV3(self, "certificates")
|
|
66
|
-
self.framework.observe(
|
|
67
|
-
self.certificates.on.certificate_request,
|
|
68
|
-
self._on_certificate_request
|
|
69
|
-
)
|
|
70
|
-
self.framework.observe(
|
|
71
|
-
self.certificates.on.certificate_revocation_request,
|
|
72
|
-
self._on_certificate_revocation_request
|
|
73
|
-
)
|
|
74
|
-
self.framework.observe(self.on.install, self._on_install)
|
|
75
|
-
|
|
76
|
-
def _on_install(self, event: InstallEvent) -> None:
|
|
77
|
-
private_key_password = b"banana"
|
|
78
|
-
private_key = generate_private_key(password=private_key_password)
|
|
79
|
-
ca_certificate = generate_ca(private_key=private_key, subject="whatever")
|
|
80
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
81
|
-
if not replicas_relation:
|
|
82
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
83
|
-
event.defer()
|
|
84
|
-
return
|
|
85
|
-
replicas_relation.data[self.app].update(
|
|
86
|
-
{
|
|
87
|
-
"private_key_password": "banana",
|
|
88
|
-
"private_key": private_key,
|
|
89
|
-
"ca_certificate": ca_certificate,
|
|
90
|
-
}
|
|
91
|
-
)
|
|
92
|
-
self.unit.status = ActiveStatus()
|
|
93
|
-
|
|
94
|
-
def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
|
|
95
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
96
|
-
if not replicas_relation:
|
|
97
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
98
|
-
event.defer()
|
|
99
|
-
return
|
|
100
|
-
ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
|
|
101
|
-
private_key = replicas_relation.data[self.app].get("private_key")
|
|
102
|
-
certificate = generate_certificate(
|
|
103
|
-
ca=ca_certificate,
|
|
104
|
-
private_key=private_key,
|
|
105
|
-
csr=event.certificate_signing_request,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
self.certificates.set_relation_certificate(
|
|
109
|
-
certificate=certificate,
|
|
110
|
-
certificate_signing_request=event.certificate_signing_request,
|
|
111
|
-
ca=ca_certificate,
|
|
112
|
-
chain=[certificate, ca_certificate],
|
|
113
|
-
relation_id=event.relation_id,
|
|
114
|
-
recommended_expiry_notification_time=720,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
|
|
118
|
-
# Do what you want to do with this information
|
|
119
|
-
pass
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if __name__ == "__main__":
|
|
123
|
-
main(ExampleProviderCharm)
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Requirer charm
|
|
127
|
-
The requirer charm is the charm requiring certificates from another charm that provides them. In
|
|
128
|
-
this example, the requirer charm is storing its certificates using a peer relation interface called
|
|
129
|
-
`replicas`.
|
|
130
|
-
|
|
131
|
-
Example:
|
|
132
|
-
```python
|
|
133
|
-
from charms.tls_certificates_interface.v3.tls_certificates import (
|
|
134
|
-
CertificateAvailableEvent,
|
|
135
|
-
CertificateExpiringEvent,
|
|
136
|
-
CertificateRevokedEvent,
|
|
137
|
-
TLSCertificatesRequiresV3,
|
|
138
|
-
generate_csr,
|
|
139
|
-
generate_private_key,
|
|
140
|
-
)
|
|
141
|
-
from ops.charm import CharmBase, RelationCreatedEvent
|
|
142
|
-
from ops.main import main
|
|
143
|
-
from ops.model import ActiveStatus, WaitingStatus
|
|
144
|
-
from typing import Union
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
class ExampleRequirerCharm(CharmBase):
|
|
148
|
-
|
|
149
|
-
def __init__(self, *args):
|
|
150
|
-
super().__init__(*args)
|
|
151
|
-
self.cert_subject = "whatever"
|
|
152
|
-
self.certificates = TLSCertificatesRequiresV3(self, "certificates")
|
|
153
|
-
self.framework.observe(self.on.install, self._on_install)
|
|
154
|
-
self.framework.observe(
|
|
155
|
-
self.on.certificates_relation_created, self._on_certificates_relation_created
|
|
156
|
-
)
|
|
157
|
-
self.framework.observe(
|
|
158
|
-
self.certificates.on.certificate_available, self._on_certificate_available
|
|
159
|
-
)
|
|
160
|
-
self.framework.observe(
|
|
161
|
-
self.certificates.on.certificate_expiring, self._on_certificate_expiring
|
|
162
|
-
)
|
|
163
|
-
self.framework.observe(
|
|
164
|
-
self.certificates.on.certificate_invalidated, self._on_certificate_invalidated
|
|
165
|
-
)
|
|
166
|
-
self.framework.observe(
|
|
167
|
-
self.certificates.on.all_certificates_invalidated,
|
|
168
|
-
self._on_all_certificates_invalidated
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
def _on_install(self, event) -> None:
|
|
172
|
-
private_key_password = b"banana"
|
|
173
|
-
private_key = generate_private_key(password=private_key_password)
|
|
174
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
175
|
-
if not replicas_relation:
|
|
176
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
177
|
-
event.defer()
|
|
178
|
-
return
|
|
179
|
-
replicas_relation.data[self.app].update(
|
|
180
|
-
{"private_key_password": "banana", "private_key": private_key.decode()}
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
def _on_certificates_relation_created(self, event: RelationCreatedEvent) -> None:
|
|
184
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
185
|
-
if not replicas_relation:
|
|
186
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
187
|
-
event.defer()
|
|
188
|
-
return
|
|
189
|
-
private_key_password = replicas_relation.data[self.app].get("private_key_password")
|
|
190
|
-
private_key = replicas_relation.data[self.app].get("private_key")
|
|
191
|
-
csr = generate_csr(
|
|
192
|
-
private_key=private_key.encode(),
|
|
193
|
-
private_key_password=private_key_password.encode(),
|
|
194
|
-
subject=self.cert_subject,
|
|
195
|
-
)
|
|
196
|
-
replicas_relation.data[self.app].update({"csr": csr.decode()})
|
|
197
|
-
self.certificates.request_certificate_creation(certificate_signing_request=csr)
|
|
198
|
-
|
|
199
|
-
def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
|
|
200
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
201
|
-
if not replicas_relation:
|
|
202
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
203
|
-
event.defer()
|
|
204
|
-
return
|
|
205
|
-
replicas_relation.data[self.app].update({"certificate": event.certificate})
|
|
206
|
-
replicas_relation.data[self.app].update({"ca": event.ca})
|
|
207
|
-
replicas_relation.data[self.app].update({"chain": event.chain})
|
|
208
|
-
self.unit.status = ActiveStatus()
|
|
209
|
-
|
|
210
|
-
def _on_certificate_expiring(
|
|
211
|
-
self, event: Union[CertificateExpiringEvent, CertificateInvalidatedEvent]
|
|
212
|
-
) -> None:
|
|
213
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
214
|
-
if not replicas_relation:
|
|
215
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
216
|
-
event.defer()
|
|
217
|
-
return
|
|
218
|
-
old_csr = replicas_relation.data[self.app].get("csr")
|
|
219
|
-
private_key_password = replicas_relation.data[self.app].get("private_key_password")
|
|
220
|
-
private_key = replicas_relation.data[self.app].get("private_key")
|
|
221
|
-
new_csr = generate_csr(
|
|
222
|
-
private_key=private_key.encode(),
|
|
223
|
-
private_key_password=private_key_password.encode(),
|
|
224
|
-
subject=self.cert_subject,
|
|
225
|
-
)
|
|
226
|
-
self.certificates.request_certificate_renewal(
|
|
227
|
-
old_certificate_signing_request=old_csr,
|
|
228
|
-
new_certificate_signing_request=new_csr,
|
|
229
|
-
)
|
|
230
|
-
replicas_relation.data[self.app].update({"csr": new_csr.decode()})
|
|
231
|
-
|
|
232
|
-
def _certificate_revoked(self) -> None:
|
|
233
|
-
old_csr = replicas_relation.data[self.app].get("csr")
|
|
234
|
-
private_key_password = replicas_relation.data[self.app].get("private_key_password")
|
|
235
|
-
private_key = replicas_relation.data[self.app].get("private_key")
|
|
236
|
-
new_csr = generate_csr(
|
|
237
|
-
private_key=private_key.encode(),
|
|
238
|
-
private_key_password=private_key_password.encode(),
|
|
239
|
-
subject=self.cert_subject,
|
|
240
|
-
)
|
|
241
|
-
self.certificates.request_certificate_renewal(
|
|
242
|
-
old_certificate_signing_request=old_csr,
|
|
243
|
-
new_certificate_signing_request=new_csr,
|
|
244
|
-
)
|
|
245
|
-
replicas_relation.data[self.app].update({"csr": new_csr.decode()})
|
|
246
|
-
replicas_relation.data[self.app].pop("certificate")
|
|
247
|
-
replicas_relation.data[self.app].pop("ca")
|
|
248
|
-
replicas_relation.data[self.app].pop("chain")
|
|
249
|
-
self.unit.status = WaitingStatus("Waiting for new certificate")
|
|
250
|
-
|
|
251
|
-
def _on_certificate_invalidated(self, event: CertificateInvalidatedEvent) -> None:
|
|
252
|
-
replicas_relation = self.model.get_relation("replicas")
|
|
253
|
-
if not replicas_relation:
|
|
254
|
-
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
|
|
255
|
-
event.defer()
|
|
256
|
-
return
|
|
257
|
-
if event.reason == "revoked":
|
|
258
|
-
self._certificate_revoked()
|
|
259
|
-
if event.reason == "expired":
|
|
260
|
-
self._on_certificate_expiring(event)
|
|
261
|
-
|
|
262
|
-
def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEvent) -> None:
|
|
263
|
-
# Do what you want with this information, probably remove all certificates.
|
|
264
|
-
pass
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if __name__ == "__main__":
|
|
268
|
-
main(ExampleRequirerCharm)
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
You can relate both charms by running:
|
|
272
|
-
|
|
273
|
-
```bash
|
|
274
|
-
juju relate <tls-certificates provider charm> <tls-certificates requirer charm>
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
""" # noqa: D405, D410, D411, D214, D416
|
|
278
|
-
|
|
279
|
-
import copy
|
|
280
|
-
import ipaddress
|
|
281
|
-
import json
|
|
282
|
-
import logging
|
|
283
|
-
import uuid
|
|
284
|
-
from contextlib import suppress
|
|
285
|
-
from dataclasses import dataclass
|
|
286
|
-
from datetime import datetime, timedelta, timezone
|
|
287
|
-
from typing import List, Literal, Optional, Union
|
|
288
|
-
|
|
289
|
-
from cryptography import x509
|
|
290
|
-
from cryptography.exceptions import InvalidSignature
|
|
291
|
-
from cryptography.hazmat.primitives import hashes, serialization
|
|
292
|
-
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
293
|
-
from cryptography.x509.oid import ExtensionOID
|
|
294
|
-
from jsonschema import exceptions, validate
|
|
295
|
-
from ops.charm import (
|
|
296
|
-
CharmBase,
|
|
297
|
-
CharmEvents,
|
|
298
|
-
RelationBrokenEvent,
|
|
299
|
-
RelationChangedEvent,
|
|
300
|
-
SecretExpiredEvent,
|
|
301
|
-
)
|
|
302
|
-
from ops.framework import EventBase, EventSource, Handle, Object
|
|
303
|
-
from ops.jujuversion import JujuVersion
|
|
304
|
-
from ops.model import (
|
|
305
|
-
Application,
|
|
306
|
-
ModelError,
|
|
307
|
-
Relation,
|
|
308
|
-
RelationDataContent,
|
|
309
|
-
Secret,
|
|
310
|
-
SecretNotFoundError,
|
|
311
|
-
Unit,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
# The unique Charmhub library identifier, never change it
|
|
315
|
-
LIBID = "afd8c2bccf834997afce12c2706d2ede"
|
|
316
|
-
|
|
317
|
-
# Increment this major API version when introducing breaking changes
|
|
318
|
-
LIBAPI = 3
|
|
319
|
-
|
|
320
|
-
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
|
321
|
-
# to 0 if you are raising the major API version
|
|
322
|
-
LIBPATCH = 27
|
|
323
|
-
|
|
324
|
-
PYDEPS = ["cryptography", "jsonschema"]
|
|
325
|
-
|
|
326
|
-
REQUIRER_JSON_SCHEMA = {
|
|
327
|
-
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
328
|
-
"$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/tls_certificates/v1/schemas/requirer.json",
|
|
329
|
-
"type": "object",
|
|
330
|
-
"title": "`tls_certificates` requirer root schema",
|
|
331
|
-
"description": "The `tls_certificates` root schema comprises the entire requirer databag for this interface.", # noqa: E501
|
|
332
|
-
"examples": [
|
|
333
|
-
{
|
|
334
|
-
"certificate_signing_requests": [
|
|
335
|
-
{
|
|
336
|
-
"certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
"certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\\nAQEBBQADggEPADCCAQoCggEBAMk3raaX803cHvzlBF9LC7KORT46z4VjyU5PIaMb\\nQLIDgYKFYI0n5hf2Ra4FAHvOvEmW7bjNlHORFEmvnpcU5kPMNUyKFMTaC8LGmN8z\\nUBH3aK+0+FRvY4afn9tgj5435WqOG9QdoDJ0TJkjJbJI9M70UOgL711oU7ql6HxU\\n4d2ydFK9xAHrBwziNHgNZ72L95s4gLTXf0fAHYf15mDA9U5yc+YDubCKgTXzVySQ\\nUx73VCJLfC/XkZIh559IrnRv5G9fu6BMLEuBwAz6QAO4+/XidbKWN4r2XSq5qX4n\\n6EPQQWP8/nd4myq1kbg6Q8w68L/0YdfjCmbyf2TuoWeImdUCAwEAAaAAMA0GCSqG\\nSIb3DQEBCwUAA4IBAQBIdwraBvpYo/rl5MH1+1Um6HRg4gOdQPY5WcJy9B9tgzJz\\nittRSlRGTnhyIo6fHgq9KHrmUthNe8mMTDailKFeaqkVNVvk7l0d1/B90Kz6OfmD\\nxN0qjW53oP7y3QB5FFBM8DjqjmUnz5UePKoX4AKkDyrKWxMwGX5RoET8c/y0y9jp\\nvSq3Wh5UpaZdWbe1oVY8CqMVUEVQL2DPjtopxXFz2qACwsXkQZxWmjvZnRiP8nP8\\nbdFaEuh9Q6rZ2QdZDEtrU4AodPU3NaukFr5KlTUQt3w/cl+5//zils6G5zUWJ2pN\\ng7+t9PTvXHRkH+LnwaVnmsBFU2e05qADQbfIn7JA\\n-----END CERTIFICATE REQUEST-----\\n" # noqa: E501
|
|
340
|
-
},
|
|
341
|
-
]
|
|
342
|
-
}
|
|
343
|
-
],
|
|
344
|
-
"properties": {
|
|
345
|
-
"certificate_signing_requests": {
|
|
346
|
-
"type": "array",
|
|
347
|
-
"items": {
|
|
348
|
-
"type": "object",
|
|
349
|
-
"properties": {
|
|
350
|
-
"certificate_signing_request": {"type": "string"},
|
|
351
|
-
"ca": {"type": "boolean"},
|
|
352
|
-
},
|
|
353
|
-
"required": ["certificate_signing_request"],
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
"required": ["certificate_signing_requests"],
|
|
358
|
-
"additionalProperties": True,
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
PROVIDER_JSON_SCHEMA = {
|
|
362
|
-
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
363
|
-
"$id": "https://canonical.github.io/charm-relation-interfaces/interfaces/tls_certificates/v1/schemas/provider.json",
|
|
364
|
-
"type": "object",
|
|
365
|
-
"title": "`tls_certificates` provider root schema",
|
|
366
|
-
"description": "The `tls_certificates` root schema comprises the entire provider databag for this interface.", # noqa: E501
|
|
367
|
-
"examples": [
|
|
368
|
-
{
|
|
369
|
-
"certificates": [
|
|
370
|
-
{
|
|
371
|
-
"ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501
|
|
372
|
-
"chain": [
|
|
373
|
-
"-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",
|
|
374
|
-
"-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501, W505
|
|
375
|
-
],
|
|
376
|
-
"certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501
|
|
377
|
-
"certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501
|
|
378
|
-
}
|
|
379
|
-
]
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
"certificates": [
|
|
383
|
-
{
|
|
384
|
-
"ca": "-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501
|
|
385
|
-
"chain": [
|
|
386
|
-
"-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n",
|
|
387
|
-
"-----BEGIN CERTIFICATE-----\\nMIIDJTCCAg2gAwIBAgIUMsSK+4FGCjW6sL/EXMSxColmKw8wDQYJKoZIhvcNAQEL\\nBQAwIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdoYXRldmVyMB4XDTIyMDcyOTIx\\nMTgyN1oXDTIzMDcyOTIxMTgyN1owIDELMAkGA1UEBhMCVVMxETAPBgNVBAMMCHdo\\nYXRldmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA55N9DkgFWbJ/\\naqcdQhso7n1kFvt6j/fL1tJBvRubkiFMQJnZFtekfalN6FfRtA3jq+nx8o49e+7t\\nLCKT0xQ+wufXfOnxv6/if6HMhHTiCNPOCeztUgQ2+dfNwRhYYgB1P93wkUVjwudK\\n13qHTTZ6NtEF6EzOqhOCe6zxq6wrr422+ZqCvcggeQ5tW9xSd/8O1vNID/0MTKpy\\nET3drDtBfHmiUEIBR3T3tcy6QsIe4Rz/2sDinAcM3j7sG8uY6drh8jY3PWar9til\\nv2l4qDYSU8Qm5856AB1FVZRLRJkLxZYZNgreShAIYgEd0mcyI2EO/UvKxsIcxsXc\\nd45GhGpKkwIDAQABo1cwVTAfBgNVHQ4EGAQWBBRXBrXKh3p/aFdQjUcT/UcvICBL\\nODAhBgNVHSMEGjAYgBYEFFcGtcqHen9oV1CNRxP9Ry8gIEs4MA8GA1UdEwEB/wQF\\nMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGmCEvcoFUrT9e133SHkgF/ZAgzeIziO\\nBjfAdU4fvAVTVfzaPm0yBnGqzcHyacCzbZjKQpaKVgc5e6IaqAQtf6cZJSCiJGhS\\nJYeosWrj3dahLOUAMrXRr8G/Ybcacoqc+osKaRa2p71cC3V6u2VvcHRV7HDFGJU7\\noijbdB+WhqET6Txe67rxZCJG9Ez3EOejBJBl2PJPpy7m1Ml4RR+E8YHNzB0lcBzc\\nEoiJKlDfKSO14E2CPDonnUoWBJWjEvJys3tbvKzsRj2fnLilytPFU0gH3cEjCopi\\nzFoWRdaRuNHYCqlBmso1JFDl8h4fMmglxGNKnKRar0WeGyxb4xXBGpI=\\n-----END CERTIFICATE-----\\n", # noqa: E501, W505
|
|
388
|
-
],
|
|
389
|
-
"certificate_signing_request": "-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKYmFuYW5hLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBANWlx9wE6cW7Jkb4DZZDOZoEjk1eDBMJ+8R4pyKp\nFBeHMl1SQSDt6rAWsrfL3KOGiIHqrRY0B5H6c51L8LDuVrJG0bPmyQ6rsBo3gVke\nDSivfSLtGvHtp8lwYnIunF8r858uYmblAR0tdXQNmnQvm+6GERvURQ6sxpgZ7iLC\npPKDoPt+4GKWL10FWf0i82FgxWC2KqRZUtNbgKETQuARLig7etBmCnh20zmynorA\ncY7vrpTPAaeQpGLNqqYvKV9W6yWVY08V+nqARrFrjk3vSioZSu8ZJUdZ4d9++SGl\nbH7A6e77YDkX9i/dQ3Pa/iDtWO3tXS2MvgoxX1iSWlGNOHcCAwEAAaAAMA0GCSqG\nSIb3DQEBCwUAA4IBAQCW1fKcHessy/ZhnIwAtSLznZeZNH8LTVOzkhVd4HA7EJW+\nKVLBx8DnN7L3V2/uPJfHiOg4Rx7fi7LkJPegl3SCqJZ0N5bQS/KvDTCyLG+9E8Y+\n7wqCmWiXaH1devimXZvazilu4IC2dSks2D8DPWHgsOdVks9bme8J3KjdNMQudegc\newWZZ1Dtbd+Rn7cpKU3jURMwm4fRwGxbJ7iT5fkLlPBlyM/yFEik4SmQxFYrZCQg\n0f3v4kBefTh5yclPy5tEH+8G0LMsbbo3dJ5mPKpAShi0QEKDLd7eR1R/712lYTK4\ndi4XaEfqERgy68O4rvb4PGlJeRGS7AmL7Ss8wfAq\n-----END CERTIFICATE REQUEST-----\n", # noqa: E501
|
|
390
|
-
"certificate": "-----BEGIN CERTIFICATE-----\nMIICvDCCAaQCFFPAOD7utDTsgFrm0vS4We18OcnKMA0GCSqGSIb3DQEBCwUAMCAx\nCzAJBgNVBAYTAlVTMREwDwYDVQQDDAh3aGF0ZXZlcjAeFw0yMjA3MjkyMTE5Mzha\nFw0yMzA3MjkyMTE5MzhaMBUxEzARBgNVBAMMCmJhbmFuYS5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDVpcfcBOnFuyZG+A2WQzmaBI5NXgwTCfvE\neKciqRQXhzJdUkEg7eqwFrK3y9yjhoiB6q0WNAeR+nOdS/Cw7layRtGz5skOq7Aa\nN4FZHg0or30i7Rrx7afJcGJyLpxfK/OfLmJm5QEdLXV0DZp0L5vuhhEb1EUOrMaY\nGe4iwqTyg6D7fuBili9dBVn9IvNhYMVgtiqkWVLTW4ChE0LgES4oO3rQZgp4dtM5\nsp6KwHGO766UzwGnkKRizaqmLylfVusllWNPFfp6gEaxa45N70oqGUrvGSVHWeHf\nfvkhpWx+wOnu+2A5F/Yv3UNz2v4g7Vjt7V0tjL4KMV9YklpRjTh3AgMBAAEwDQYJ\nKoZIhvcNAQELBQADggEBAChjRzuba8zjQ7NYBVas89Oy7u++MlS8xWxh++yiUsV6\nWMk3ZemsPtXc1YmXorIQohtxLxzUPm2JhyzFzU/sOLmJQ1E/l+gtZHyRCwsb20fX\nmphuJsMVd7qv/GwEk9PBsk2uDqg4/Wix0Rx5lf95juJP7CPXQJl5FQauf3+LSz0y\nwF/j+4GqvrwsWr9hKOLmPdkyKkR6bHKtzzsxL9PM8GnElk2OpaPMMnzbL/vt2IAt\nxK01ZzPxCQCzVwHo5IJO5NR/fIyFbEPhxzG17QsRDOBR9fl9cOIvDeSO04vyZ+nz\n+kA2c3fNrZFAtpIlOOmFh8Q12rVL4sAjI5mVWnNEgvI=\n-----END CERTIFICATE-----\n", # noqa: E501
|
|
391
|
-
"revoked": True,
|
|
392
|
-
}
|
|
393
|
-
]
|
|
394
|
-
},
|
|
395
|
-
],
|
|
396
|
-
"properties": {
|
|
397
|
-
"certificates": {
|
|
398
|
-
"$id": "#/properties/certificates",
|
|
399
|
-
"type": "array",
|
|
400
|
-
"items": {
|
|
401
|
-
"$id": "#/properties/certificates/items",
|
|
402
|
-
"type": "object",
|
|
403
|
-
"required": ["certificate_signing_request", "certificate", "ca", "chain"],
|
|
404
|
-
"properties": {
|
|
405
|
-
"certificate_signing_request": {
|
|
406
|
-
"$id": "#/properties/certificates/items/certificate_signing_request",
|
|
407
|
-
"type": "string",
|
|
408
|
-
},
|
|
409
|
-
"certificate": {
|
|
410
|
-
"$id": "#/properties/certificates/items/certificate",
|
|
411
|
-
"type": "string",
|
|
412
|
-
},
|
|
413
|
-
"ca": {"$id": "#/properties/certificates/items/ca", "type": "string"},
|
|
414
|
-
"chain": {
|
|
415
|
-
"$id": "#/properties/certificates/items/chain",
|
|
416
|
-
"type": "array",
|
|
417
|
-
"items": {
|
|
418
|
-
"type": "string",
|
|
419
|
-
"$id": "#/properties/certificates/items/chain/items",
|
|
420
|
-
},
|
|
421
|
-
},
|
|
422
|
-
"revoked": {
|
|
423
|
-
"$id": "#/properties/certificates/items/revoked",
|
|
424
|
-
"type": "boolean",
|
|
425
|
-
},
|
|
426
|
-
},
|
|
427
|
-
"additionalProperties": True,
|
|
428
|
-
},
|
|
429
|
-
}
|
|
430
|
-
},
|
|
431
|
-
"required": ["certificates"],
|
|
432
|
-
"additionalProperties": True,
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
logger = logging.getLogger(__name__)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
@dataclass
|
|
440
|
-
class RequirerCSR:
|
|
441
|
-
"""This class represents a certificate signing request from an interface Requirer."""
|
|
442
|
-
|
|
443
|
-
relation_id: int
|
|
444
|
-
application_name: str
|
|
445
|
-
unit_name: str
|
|
446
|
-
csr: str
|
|
447
|
-
is_ca: bool
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
@dataclass
|
|
451
|
-
class ProviderCertificate:
|
|
452
|
-
"""This class represents a certificate from an interface Provider."""
|
|
453
|
-
|
|
454
|
-
relation_id: int
|
|
455
|
-
application_name: str
|
|
456
|
-
csr: str
|
|
457
|
-
certificate: str
|
|
458
|
-
ca: str
|
|
459
|
-
chain: List[str]
|
|
460
|
-
revoked: bool
|
|
461
|
-
expiry_time: datetime
|
|
462
|
-
expiry_notification_time: Optional[datetime] = None
|
|
463
|
-
|
|
464
|
-
def chain_as_pem(self, reverse: bool = True) -> str:
|
|
465
|
-
"""Return full certificate chain as a PEM string.
|
|
466
|
-
|
|
467
|
-
The function is deprecated, please use chain_as_pem_string instead.
|
|
468
|
-
|
|
469
|
-
Args:
|
|
470
|
-
reverse (bool): By default this function will reverse the order of the chain from relation data.
|
|
471
|
-
To disable that, set reverse to False.
|
|
472
|
-
"""
|
|
473
|
-
logger.warning("This function is deprecated, please use chain_as_pem_string instead")
|
|
474
|
-
return "\n\n".join(reversed(self.chain)) if reverse else "\n\n".join(self.chain)
|
|
475
|
-
|
|
476
|
-
def chain_as_pem_string(self) -> str:
|
|
477
|
-
"""Return full certificate chain as a PEM string."""
|
|
478
|
-
return "\n\n".join(self.chain)
|
|
479
|
-
|
|
480
|
-
def to_json(self) -> str:
|
|
481
|
-
"""Return the object as a JSON string.
|
|
482
|
-
|
|
483
|
-
Returns:
|
|
484
|
-
str: JSON representation of the object
|
|
485
|
-
"""
|
|
486
|
-
return json.dumps(
|
|
487
|
-
{
|
|
488
|
-
"relation_id": self.relation_id,
|
|
489
|
-
"application_name": self.application_name,
|
|
490
|
-
"csr": self.csr,
|
|
491
|
-
"certificate": self.certificate,
|
|
492
|
-
"ca": self.ca,
|
|
493
|
-
"chain": self.chain,
|
|
494
|
-
"revoked": self.revoked,
|
|
495
|
-
"expiry_time": self.expiry_time.isoformat(),
|
|
496
|
-
"expiry_notification_time": self.expiry_notification_time.isoformat()
|
|
497
|
-
if self.expiry_notification_time
|
|
498
|
-
else None,
|
|
499
|
-
}
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
class CertificateAvailableEvent(EventBase):
|
|
504
|
-
"""Charm Event triggered when a TLS certificate is available."""
|
|
505
|
-
|
|
506
|
-
def __init__(
|
|
507
|
-
self,
|
|
508
|
-
handle: Handle,
|
|
509
|
-
certificate: str,
|
|
510
|
-
certificate_signing_request: str,
|
|
511
|
-
ca: str,
|
|
512
|
-
chain: List[str],
|
|
513
|
-
):
|
|
514
|
-
super().__init__(handle)
|
|
515
|
-
self.certificate = certificate
|
|
516
|
-
self.certificate_signing_request = certificate_signing_request
|
|
517
|
-
self.ca = ca
|
|
518
|
-
self.chain = chain
|
|
519
|
-
|
|
520
|
-
def snapshot(self) -> dict:
|
|
521
|
-
"""Return snapshot."""
|
|
522
|
-
return {
|
|
523
|
-
"certificate": self.certificate,
|
|
524
|
-
"certificate_signing_request": self.certificate_signing_request,
|
|
525
|
-
"ca": self.ca,
|
|
526
|
-
"chain": self.chain,
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
def restore(self, snapshot: dict):
|
|
530
|
-
"""Restore snapshot."""
|
|
531
|
-
self.certificate = snapshot["certificate"]
|
|
532
|
-
self.certificate_signing_request = snapshot["certificate_signing_request"]
|
|
533
|
-
self.ca = snapshot["ca"]
|
|
534
|
-
self.chain = snapshot["chain"]
|
|
535
|
-
|
|
536
|
-
def chain_as_pem(self, reverse: bool = True) -> str:
|
|
537
|
-
"""Return full certificate chain as a PEM string.
|
|
538
|
-
|
|
539
|
-
The function is deprecated, please use chain_as_pem_string instead.
|
|
540
|
-
|
|
541
|
-
Args:
|
|
542
|
-
reverse (bool): By default this function will reverse the order of the chain from relation data.
|
|
543
|
-
To disable that, set reverse to False.
|
|
544
|
-
"""
|
|
545
|
-
logger.warning("This function is deprecated, please use chain_as_pem_string instead")
|
|
546
|
-
return "\n\n".join(reversed(self.chain)) if reverse else "\n\n".join(self.chain)
|
|
547
|
-
|
|
548
|
-
def chain_as_pem_string(self) -> str:
|
|
549
|
-
"""Return full certificate chain as a PEM string."""
|
|
550
|
-
return "\n\n".join(self.chain)
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
class CertificateExpiringEvent(EventBase):
|
|
554
|
-
"""Charm Event triggered when a TLS certificate is almost expired."""
|
|
555
|
-
|
|
556
|
-
def __init__(self, handle: Handle, certificate: str, expiry: str):
|
|
557
|
-
"""CertificateExpiringEvent.
|
|
558
|
-
|
|
559
|
-
Args:
|
|
560
|
-
handle (Handle): Juju framework handle
|
|
561
|
-
certificate (str): TLS Certificate
|
|
562
|
-
expiry (str): Datetime string representing the time at which the certificate
|
|
563
|
-
won't be valid anymore.
|
|
564
|
-
"""
|
|
565
|
-
super().__init__(handle)
|
|
566
|
-
self.certificate = certificate
|
|
567
|
-
self.expiry = expiry
|
|
568
|
-
|
|
569
|
-
def snapshot(self) -> dict:
|
|
570
|
-
"""Return snapshot."""
|
|
571
|
-
return {"certificate": self.certificate, "expiry": self.expiry}
|
|
572
|
-
|
|
573
|
-
def restore(self, snapshot: dict):
|
|
574
|
-
"""Restore snapshot."""
|
|
575
|
-
self.certificate = snapshot["certificate"]
|
|
576
|
-
self.expiry = snapshot["expiry"]
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
class CertificateInvalidatedEvent(EventBase):
|
|
580
|
-
"""Charm Event triggered when a TLS certificate is invalidated."""
|
|
581
|
-
|
|
582
|
-
def __init__(
|
|
583
|
-
self,
|
|
584
|
-
handle: Handle,
|
|
585
|
-
reason: Literal["expired", "revoked"],
|
|
586
|
-
certificate: str,
|
|
587
|
-
certificate_signing_request: str,
|
|
588
|
-
ca: str,
|
|
589
|
-
chain: List[str],
|
|
590
|
-
):
|
|
591
|
-
super().__init__(handle)
|
|
592
|
-
self.reason = reason
|
|
593
|
-
self.certificate_signing_request = certificate_signing_request
|
|
594
|
-
self.certificate = certificate
|
|
595
|
-
self.ca = ca
|
|
596
|
-
self.chain = chain
|
|
597
|
-
|
|
598
|
-
def snapshot(self) -> dict:
|
|
599
|
-
"""Return snapshot."""
|
|
600
|
-
return {
|
|
601
|
-
"reason": self.reason,
|
|
602
|
-
"certificate_signing_request": self.certificate_signing_request,
|
|
603
|
-
"certificate": self.certificate,
|
|
604
|
-
"ca": self.ca,
|
|
605
|
-
"chain": self.chain,
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
def restore(self, snapshot: dict):
|
|
609
|
-
"""Restore snapshot."""
|
|
610
|
-
self.reason = snapshot["reason"]
|
|
611
|
-
self.certificate_signing_request = snapshot["certificate_signing_request"]
|
|
612
|
-
self.certificate = snapshot["certificate"]
|
|
613
|
-
self.ca = snapshot["ca"]
|
|
614
|
-
self.chain = snapshot["chain"]
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
class AllCertificatesInvalidatedEvent(EventBase):
|
|
618
|
-
"""Charm Event triggered when all TLS certificates are invalidated."""
|
|
619
|
-
|
|
620
|
-
def __init__(self, handle: Handle):
|
|
621
|
-
super().__init__(handle)
|
|
622
|
-
|
|
623
|
-
def snapshot(self) -> dict:
|
|
624
|
-
"""Return snapshot."""
|
|
625
|
-
return {}
|
|
626
|
-
|
|
627
|
-
def restore(self, snapshot: dict):
|
|
628
|
-
"""Restore snapshot."""
|
|
629
|
-
pass
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
class CertificateCreationRequestEvent(EventBase):
|
|
633
|
-
"""Charm Event triggered when a TLS certificate is required."""
|
|
634
|
-
|
|
635
|
-
def __init__(
|
|
636
|
-
self,
|
|
637
|
-
handle: Handle,
|
|
638
|
-
certificate_signing_request: str,
|
|
639
|
-
relation_id: int,
|
|
640
|
-
is_ca: bool = False,
|
|
641
|
-
):
|
|
642
|
-
super().__init__(handle)
|
|
643
|
-
self.certificate_signing_request = certificate_signing_request
|
|
644
|
-
self.relation_id = relation_id
|
|
645
|
-
self.is_ca = is_ca
|
|
646
|
-
|
|
647
|
-
def snapshot(self) -> dict:
|
|
648
|
-
"""Return snapshot."""
|
|
649
|
-
return {
|
|
650
|
-
"certificate_signing_request": self.certificate_signing_request,
|
|
651
|
-
"relation_id": self.relation_id,
|
|
652
|
-
"is_ca": self.is_ca,
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
def restore(self, snapshot: dict):
|
|
656
|
-
"""Restore snapshot."""
|
|
657
|
-
self.certificate_signing_request = snapshot["certificate_signing_request"]
|
|
658
|
-
self.relation_id = snapshot["relation_id"]
|
|
659
|
-
self.is_ca = snapshot["is_ca"]
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
class CertificateRevocationRequestEvent(EventBase):
|
|
663
|
-
"""Charm Event triggered when a TLS certificate needs to be revoked."""
|
|
664
|
-
|
|
665
|
-
def __init__(
|
|
666
|
-
self,
|
|
667
|
-
handle: Handle,
|
|
668
|
-
certificate: str,
|
|
669
|
-
certificate_signing_request: str,
|
|
670
|
-
ca: str,
|
|
671
|
-
chain: str,
|
|
672
|
-
):
|
|
673
|
-
super().__init__(handle)
|
|
674
|
-
self.certificate = certificate
|
|
675
|
-
self.certificate_signing_request = certificate_signing_request
|
|
676
|
-
self.ca = ca
|
|
677
|
-
self.chain = chain
|
|
678
|
-
|
|
679
|
-
def snapshot(self) -> dict:
|
|
680
|
-
"""Return snapshot."""
|
|
681
|
-
return {
|
|
682
|
-
"certificate": self.certificate,
|
|
683
|
-
"certificate_signing_request": self.certificate_signing_request,
|
|
684
|
-
"ca": self.ca,
|
|
685
|
-
"chain": self.chain,
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
def restore(self, snapshot: dict):
|
|
689
|
-
"""Restore snapshot."""
|
|
690
|
-
self.certificate = snapshot["certificate"]
|
|
691
|
-
self.certificate_signing_request = snapshot["certificate_signing_request"]
|
|
692
|
-
self.ca = snapshot["ca"]
|
|
693
|
-
self.chain = snapshot["chain"]
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
def chain_has_valid_order(chain: List[str]) -> bool:
|
|
697
|
-
"""Check if the chain has a valid order.
|
|
698
|
-
|
|
699
|
-
Validates that each certificate in the chain is properly signed by the next certificate.
|
|
700
|
-
The chain should be ordered from leaf to root, where each certificate is signed by
|
|
701
|
-
the next one in the chain.
|
|
702
|
-
|
|
703
|
-
Args:
|
|
704
|
-
chain (List[str]): List of certificates in PEM format, ordered from leaf to root
|
|
705
|
-
|
|
706
|
-
Returns:
|
|
707
|
-
bool: True if the chain has a valid order, False otherwise.
|
|
708
|
-
"""
|
|
709
|
-
if len(chain) < 2:
|
|
710
|
-
return True
|
|
711
|
-
|
|
712
|
-
try:
|
|
713
|
-
for i in range(len(chain) - 1):
|
|
714
|
-
cert = x509.load_pem_x509_certificate(chain[i].encode())
|
|
715
|
-
issuer = x509.load_pem_x509_certificate(chain[i + 1].encode())
|
|
716
|
-
cert.verify_directly_issued_by(issuer)
|
|
717
|
-
return True
|
|
718
|
-
except (ValueError, TypeError, InvalidSignature):
|
|
719
|
-
return False
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def _load_relation_data(relation_data_content: RelationDataContent) -> dict:
|
|
723
|
-
"""Load relation data from the relation data bag.
|
|
724
|
-
|
|
725
|
-
Json loads all data.
|
|
726
|
-
|
|
727
|
-
Args:
|
|
728
|
-
relation_data_content: Relation data from the databag
|
|
729
|
-
|
|
730
|
-
Returns:
|
|
731
|
-
dict: Relation data in dict format.
|
|
732
|
-
"""
|
|
733
|
-
certificate_data = {}
|
|
734
|
-
try:
|
|
735
|
-
for key in relation_data_content:
|
|
736
|
-
try:
|
|
737
|
-
certificate_data[key] = json.loads(relation_data_content[key])
|
|
738
|
-
except (json.decoder.JSONDecodeError, TypeError):
|
|
739
|
-
certificate_data[key] = relation_data_content[key]
|
|
740
|
-
except ModelError:
|
|
741
|
-
pass
|
|
742
|
-
return certificate_data
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
def _get_closest_future_time(
|
|
746
|
-
expiry_notification_time: datetime, expiry_time: datetime
|
|
747
|
-
) -> datetime:
|
|
748
|
-
"""Return expiry_notification_time if not in the past, otherwise return expiry_time.
|
|
749
|
-
|
|
750
|
-
Args:
|
|
751
|
-
expiry_notification_time (datetime): Notification time of impending expiration
|
|
752
|
-
expiry_time (datetime): Expiration time
|
|
753
|
-
|
|
754
|
-
Returns:
|
|
755
|
-
datetime: expiry_notification_time if not in the past, expiry_time otherwise
|
|
756
|
-
"""
|
|
757
|
-
return (
|
|
758
|
-
expiry_notification_time
|
|
759
|
-
if datetime.now(timezone.utc) < expiry_notification_time
|
|
760
|
-
else expiry_time
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
def calculate_expiry_notification_time(
|
|
765
|
-
validity_start_time: datetime,
|
|
766
|
-
expiry_time: datetime,
|
|
767
|
-
provider_recommended_notification_time: Optional[int],
|
|
768
|
-
requirer_recommended_notification_time: Optional[int],
|
|
769
|
-
) -> datetime:
|
|
770
|
-
"""Calculate a reasonable time to notify the user about the certificate expiry.
|
|
771
|
-
|
|
772
|
-
It takes into account the time recommended by the provider and by the requirer.
|
|
773
|
-
Time recommended by the provider is preferred,
|
|
774
|
-
then time recommended by the requirer,
|
|
775
|
-
then dynamically calculated time.
|
|
776
|
-
|
|
777
|
-
Args:
|
|
778
|
-
validity_start_time: Certificate validity time
|
|
779
|
-
expiry_time: Certificate expiry time
|
|
780
|
-
provider_recommended_notification_time:
|
|
781
|
-
Time in hours prior to expiry to notify the user.
|
|
782
|
-
Recommended by the provider.
|
|
783
|
-
requirer_recommended_notification_time:
|
|
784
|
-
Time in hours prior to expiry to notify the user.
|
|
785
|
-
Recommended by the requirer.
|
|
786
|
-
|
|
787
|
-
Returns:
|
|
788
|
-
datetime: Time to notify the user about the certificate expiry.
|
|
789
|
-
"""
|
|
790
|
-
if provider_recommended_notification_time is not None:
|
|
791
|
-
provider_recommended_notification_time = abs(provider_recommended_notification_time)
|
|
792
|
-
provider_recommendation_time_delta = expiry_time - timedelta(
|
|
793
|
-
hours=provider_recommended_notification_time
|
|
794
|
-
)
|
|
795
|
-
if validity_start_time < provider_recommendation_time_delta:
|
|
796
|
-
return provider_recommendation_time_delta
|
|
797
|
-
|
|
798
|
-
if requirer_recommended_notification_time is not None:
|
|
799
|
-
requirer_recommended_notification_time = abs(requirer_recommended_notification_time)
|
|
800
|
-
requirer_recommendation_time_delta = expiry_time - timedelta(
|
|
801
|
-
hours=requirer_recommended_notification_time
|
|
802
|
-
)
|
|
803
|
-
if validity_start_time < requirer_recommendation_time_delta:
|
|
804
|
-
return requirer_recommendation_time_delta
|
|
805
|
-
calculated_hours = (expiry_time - validity_start_time).total_seconds() / (3600 * 3)
|
|
806
|
-
return expiry_time - timedelta(hours=calculated_hours)
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
def generate_ca(
|
|
810
|
-
private_key: bytes,
|
|
811
|
-
subject: str,
|
|
812
|
-
private_key_password: Optional[bytes] = None,
|
|
813
|
-
validity: int = 365,
|
|
814
|
-
country: str = "US",
|
|
815
|
-
) -> bytes:
|
|
816
|
-
"""Generate a CA Certificate.
|
|
817
|
-
|
|
818
|
-
Args:
|
|
819
|
-
private_key (bytes): Private key
|
|
820
|
-
subject (str): Common Name that can be an IP or a Full Qualified Domain Name (FQDN).
|
|
821
|
-
private_key_password (bytes): Private key password
|
|
822
|
-
validity (int): Certificate validity time (in days)
|
|
823
|
-
country (str): Certificate Issuing country
|
|
824
|
-
|
|
825
|
-
Returns:
|
|
826
|
-
bytes: CA Certificate.
|
|
827
|
-
"""
|
|
828
|
-
private_key_object = serialization.load_pem_private_key(
|
|
829
|
-
private_key, password=private_key_password
|
|
830
|
-
)
|
|
831
|
-
subject_name = x509.Name(
|
|
832
|
-
[
|
|
833
|
-
x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country),
|
|
834
|
-
x509.NameAttribute(x509.NameOID.COMMON_NAME, subject),
|
|
835
|
-
]
|
|
836
|
-
)
|
|
837
|
-
subject_identifier_object = x509.SubjectKeyIdentifier.from_public_key(
|
|
838
|
-
private_key_object.public_key() # type: ignore[arg-type]
|
|
839
|
-
)
|
|
840
|
-
subject_identifier = key_identifier = subject_identifier_object.public_bytes()
|
|
841
|
-
key_usage = x509.KeyUsage(
|
|
842
|
-
digital_signature=True,
|
|
843
|
-
key_encipherment=True,
|
|
844
|
-
key_cert_sign=True,
|
|
845
|
-
key_agreement=False,
|
|
846
|
-
content_commitment=False,
|
|
847
|
-
data_encipherment=False,
|
|
848
|
-
crl_sign=False,
|
|
849
|
-
encipher_only=False,
|
|
850
|
-
decipher_only=False,
|
|
851
|
-
)
|
|
852
|
-
cert = (
|
|
853
|
-
x509.CertificateBuilder()
|
|
854
|
-
.subject_name(subject_name)
|
|
855
|
-
.issuer_name(subject_name)
|
|
856
|
-
.public_key(private_key_object.public_key()) # type: ignore[arg-type]
|
|
857
|
-
.serial_number(x509.random_serial_number())
|
|
858
|
-
.not_valid_before(datetime.now(timezone.utc))
|
|
859
|
-
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=validity))
|
|
860
|
-
.add_extension(x509.SubjectKeyIdentifier(digest=subject_identifier), critical=False)
|
|
861
|
-
.add_extension(
|
|
862
|
-
x509.AuthorityKeyIdentifier(
|
|
863
|
-
key_identifier=key_identifier,
|
|
864
|
-
authority_cert_issuer=None,
|
|
865
|
-
authority_cert_serial_number=None,
|
|
866
|
-
),
|
|
867
|
-
critical=False,
|
|
868
|
-
)
|
|
869
|
-
.add_extension(key_usage, critical=True)
|
|
870
|
-
.add_extension(
|
|
871
|
-
x509.BasicConstraints(ca=True, path_length=None),
|
|
872
|
-
critical=True,
|
|
873
|
-
)
|
|
874
|
-
.sign(private_key_object, hashes.SHA256()) # type: ignore[arg-type]
|
|
875
|
-
)
|
|
876
|
-
return cert.public_bytes(serialization.Encoding.PEM)
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
def get_certificate_extensions(
|
|
880
|
-
authority_key_identifier: bytes,
|
|
881
|
-
csr: x509.CertificateSigningRequest,
|
|
882
|
-
alt_names: Optional[List[str]],
|
|
883
|
-
is_ca: bool,
|
|
884
|
-
) -> List[x509.Extension]:
|
|
885
|
-
"""Generate a list of certificate extensions from a CSR and other known information.
|
|
886
|
-
|
|
887
|
-
Args:
|
|
888
|
-
authority_key_identifier (bytes): Authority key identifier
|
|
889
|
-
csr (x509.CertificateSigningRequest): CSR
|
|
890
|
-
alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
|
|
891
|
-
is_ca (bool): Whether the certificate is a CA certificate
|
|
892
|
-
|
|
893
|
-
Returns:
|
|
894
|
-
List[x509.Extension]: List of extensions
|
|
895
|
-
"""
|
|
896
|
-
cert_extensions_list: List[x509.Extension] = [
|
|
897
|
-
x509.Extension(
|
|
898
|
-
oid=ExtensionOID.AUTHORITY_KEY_IDENTIFIER,
|
|
899
|
-
value=x509.AuthorityKeyIdentifier(
|
|
900
|
-
key_identifier=authority_key_identifier,
|
|
901
|
-
authority_cert_issuer=None,
|
|
902
|
-
authority_cert_serial_number=None,
|
|
903
|
-
),
|
|
904
|
-
critical=False,
|
|
905
|
-
),
|
|
906
|
-
x509.Extension(
|
|
907
|
-
oid=ExtensionOID.SUBJECT_KEY_IDENTIFIER,
|
|
908
|
-
value=x509.SubjectKeyIdentifier.from_public_key(csr.public_key()),
|
|
909
|
-
critical=False,
|
|
910
|
-
),
|
|
911
|
-
x509.Extension(
|
|
912
|
-
oid=ExtensionOID.BASIC_CONSTRAINTS,
|
|
913
|
-
critical=True,
|
|
914
|
-
value=x509.BasicConstraints(ca=is_ca, path_length=None),
|
|
915
|
-
),
|
|
916
|
-
]
|
|
917
|
-
|
|
918
|
-
sans: List[x509.GeneralName] = []
|
|
919
|
-
san_alt_names = [x509.DNSName(name) for name in alt_names] if alt_names else []
|
|
920
|
-
sans.extend(san_alt_names)
|
|
921
|
-
try:
|
|
922
|
-
loaded_san_ext = csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
923
|
-
sans.extend(
|
|
924
|
-
[x509.DNSName(name) for name in loaded_san_ext.value.get_values_for_type(x509.DNSName)]
|
|
925
|
-
)
|
|
926
|
-
sans.extend(
|
|
927
|
-
[x509.IPAddress(ip) for ip in loaded_san_ext.value.get_values_for_type(x509.IPAddress)]
|
|
928
|
-
)
|
|
929
|
-
sans.extend(
|
|
930
|
-
[
|
|
931
|
-
x509.RegisteredID(oid)
|
|
932
|
-
for oid in loaded_san_ext.value.get_values_for_type(x509.RegisteredID)
|
|
933
|
-
]
|
|
934
|
-
)
|
|
935
|
-
except x509.ExtensionNotFound:
|
|
936
|
-
pass
|
|
937
|
-
|
|
938
|
-
if sans:
|
|
939
|
-
cert_extensions_list.append(
|
|
940
|
-
x509.Extension(
|
|
941
|
-
oid=ExtensionOID.SUBJECT_ALTERNATIVE_NAME,
|
|
942
|
-
critical=False,
|
|
943
|
-
value=x509.SubjectAlternativeName(sans),
|
|
944
|
-
)
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
if is_ca:
|
|
948
|
-
cert_extensions_list.append(
|
|
949
|
-
x509.Extension(
|
|
950
|
-
ExtensionOID.KEY_USAGE,
|
|
951
|
-
critical=True,
|
|
952
|
-
value=x509.KeyUsage(
|
|
953
|
-
digital_signature=False,
|
|
954
|
-
content_commitment=False,
|
|
955
|
-
key_encipherment=False,
|
|
956
|
-
data_encipherment=False,
|
|
957
|
-
key_agreement=False,
|
|
958
|
-
key_cert_sign=True,
|
|
959
|
-
crl_sign=True,
|
|
960
|
-
encipher_only=False,
|
|
961
|
-
decipher_only=False,
|
|
962
|
-
),
|
|
963
|
-
)
|
|
964
|
-
)
|
|
965
|
-
|
|
966
|
-
existing_oids = {ext.oid for ext in cert_extensions_list}
|
|
967
|
-
for extension in csr.extensions:
|
|
968
|
-
if extension.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
|
|
969
|
-
continue
|
|
970
|
-
if extension.oid in existing_oids:
|
|
971
|
-
logger.warning("Extension %s is managed by the TLS provider, ignoring.", extension.oid)
|
|
972
|
-
continue
|
|
973
|
-
cert_extensions_list.append(extension)
|
|
974
|
-
|
|
975
|
-
return cert_extensions_list
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
def generate_certificate(
|
|
979
|
-
csr: bytes,
|
|
980
|
-
ca: bytes,
|
|
981
|
-
ca_key: bytes,
|
|
982
|
-
ca_key_password: Optional[bytes] = None,
|
|
983
|
-
validity: int = 365,
|
|
984
|
-
alt_names: Optional[List[str]] = None,
|
|
985
|
-
is_ca: bool = False,
|
|
986
|
-
) -> bytes:
|
|
987
|
-
"""Generate a TLS certificate based on a CSR.
|
|
988
|
-
|
|
989
|
-
Args:
|
|
990
|
-
csr (bytes): CSR
|
|
991
|
-
ca (bytes): CA Certificate
|
|
992
|
-
ca_key (bytes): CA private key
|
|
993
|
-
ca_key_password: CA private key password
|
|
994
|
-
validity (int): Certificate validity (in days)
|
|
995
|
-
alt_names (list): List of alt names to put on cert - prefer putting SANs in CSR
|
|
996
|
-
is_ca (bool): Whether the certificate is a CA certificate
|
|
997
|
-
|
|
998
|
-
Returns:
|
|
999
|
-
bytes: Certificate
|
|
1000
|
-
"""
|
|
1001
|
-
csr_object = x509.load_pem_x509_csr(csr)
|
|
1002
|
-
subject = csr_object.subject
|
|
1003
|
-
ca_pem = x509.load_pem_x509_certificate(ca)
|
|
1004
|
-
issuer = ca_pem.issuer
|
|
1005
|
-
private_key = serialization.load_pem_private_key(ca_key, password=ca_key_password)
|
|
1006
|
-
|
|
1007
|
-
certificate_builder = (
|
|
1008
|
-
x509.CertificateBuilder()
|
|
1009
|
-
.subject_name(subject)
|
|
1010
|
-
.issuer_name(issuer)
|
|
1011
|
-
.public_key(csr_object.public_key())
|
|
1012
|
-
.serial_number(x509.random_serial_number())
|
|
1013
|
-
.not_valid_before(datetime.now(timezone.utc))
|
|
1014
|
-
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=validity))
|
|
1015
|
-
)
|
|
1016
|
-
extensions = get_certificate_extensions(
|
|
1017
|
-
authority_key_identifier=ca_pem.extensions.get_extension_for_class(
|
|
1018
|
-
x509.SubjectKeyIdentifier
|
|
1019
|
-
).value.key_identifier,
|
|
1020
|
-
csr=csr_object,
|
|
1021
|
-
alt_names=alt_names,
|
|
1022
|
-
is_ca=is_ca,
|
|
1023
|
-
)
|
|
1024
|
-
for extension in extensions:
|
|
1025
|
-
try:
|
|
1026
|
-
certificate_builder = certificate_builder.add_extension(
|
|
1027
|
-
extval=extension.value,
|
|
1028
|
-
critical=extension.critical,
|
|
1029
|
-
)
|
|
1030
|
-
except ValueError as e:
|
|
1031
|
-
logger.warning("Failed to add extension %s: %s", extension.oid, e)
|
|
1032
|
-
|
|
1033
|
-
cert = certificate_builder.sign(private_key, hashes.SHA256()) # type: ignore[arg-type]
|
|
1034
|
-
return cert.public_bytes(serialization.Encoding.PEM)
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
def generate_private_key(
|
|
1038
|
-
password: Optional[bytes] = None,
|
|
1039
|
-
key_size: int = 2048,
|
|
1040
|
-
public_exponent: int = 65537,
|
|
1041
|
-
) -> bytes:
|
|
1042
|
-
"""Generate a private key.
|
|
1043
|
-
|
|
1044
|
-
Args:
|
|
1045
|
-
password (bytes): Password for decrypting the private key
|
|
1046
|
-
key_size (int): Key size in bytes
|
|
1047
|
-
public_exponent: Public exponent.
|
|
1048
|
-
|
|
1049
|
-
Returns:
|
|
1050
|
-
bytes: Private Key
|
|
1051
|
-
"""
|
|
1052
|
-
private_key = rsa.generate_private_key(
|
|
1053
|
-
public_exponent=public_exponent,
|
|
1054
|
-
key_size=key_size,
|
|
1055
|
-
)
|
|
1056
|
-
key_bytes = private_key.private_bytes(
|
|
1057
|
-
encoding=serialization.Encoding.PEM,
|
|
1058
|
-
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
1059
|
-
encryption_algorithm=(
|
|
1060
|
-
serialization.BestAvailableEncryption(password)
|
|
1061
|
-
if password
|
|
1062
|
-
else serialization.NoEncryption()
|
|
1063
|
-
),
|
|
1064
|
-
)
|
|
1065
|
-
return key_bytes
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
def generate_csr( # noqa: C901
|
|
1069
|
-
private_key: bytes,
|
|
1070
|
-
subject: str,
|
|
1071
|
-
add_unique_id_to_subject_name: bool = True,
|
|
1072
|
-
organization: Optional[str] = None,
|
|
1073
|
-
email_address: Optional[str] = None,
|
|
1074
|
-
country_name: Optional[str] = None,
|
|
1075
|
-
state_or_province_name: Optional[str] = None,
|
|
1076
|
-
locality_name: Optional[str] = None,
|
|
1077
|
-
private_key_password: Optional[bytes] = None,
|
|
1078
|
-
sans: Optional[List[str]] = None,
|
|
1079
|
-
sans_oid: Optional[List[str]] = None,
|
|
1080
|
-
sans_ip: Optional[List[str]] = None,
|
|
1081
|
-
sans_dns: Optional[List[str]] = None,
|
|
1082
|
-
additional_critical_extensions: Optional[List] = None,
|
|
1083
|
-
) -> bytes:
|
|
1084
|
-
"""Generate a CSR using private key and subject.
|
|
1085
|
-
|
|
1086
|
-
Args:
|
|
1087
|
-
private_key (bytes): Private key
|
|
1088
|
-
subject (str): CSR Common Name that can be an IP or a Full Qualified Domain Name (FQDN).
|
|
1089
|
-
add_unique_id_to_subject_name (bool): Whether a unique ID must be added to the CSR's
|
|
1090
|
-
subject name. Always leave to "True" when the CSR is used to request certificates
|
|
1091
|
-
using the tls-certificates relation.
|
|
1092
|
-
organization (str): Name of organization.
|
|
1093
|
-
email_address (str): Email address.
|
|
1094
|
-
country_name (str): Country Name.
|
|
1095
|
-
state_or_province_name (str): State or Province Name.
|
|
1096
|
-
locality_name (str): Locality Name.
|
|
1097
|
-
private_key_password (bytes): Private key password
|
|
1098
|
-
sans (list): Use sans_dns - this will be deprecated in a future release
|
|
1099
|
-
List of DNS subject alternative names (keeping it for now for backward compatibility)
|
|
1100
|
-
sans_oid (list): List of registered ID SANs
|
|
1101
|
-
sans_dns (list): List of DNS subject alternative names (similar to the arg: sans)
|
|
1102
|
-
sans_ip (list): List of IP subject alternative names
|
|
1103
|
-
additional_critical_extensions (list): List of critical additional extension objects.
|
|
1104
|
-
Object must be a x509 ExtensionType.
|
|
1105
|
-
|
|
1106
|
-
Returns:
|
|
1107
|
-
bytes: CSR
|
|
1108
|
-
"""
|
|
1109
|
-
signing_key = serialization.load_pem_private_key(private_key, password=private_key_password)
|
|
1110
|
-
subject_name = [x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)]
|
|
1111
|
-
if add_unique_id_to_subject_name:
|
|
1112
|
-
unique_identifier = uuid.uuid4()
|
|
1113
|
-
subject_name.append(
|
|
1114
|
-
x509.NameAttribute(x509.NameOID.X500_UNIQUE_IDENTIFIER, str(unique_identifier))
|
|
1115
|
-
)
|
|
1116
|
-
if organization:
|
|
1117
|
-
subject_name.append(x509.NameAttribute(x509.NameOID.ORGANIZATION_NAME, organization))
|
|
1118
|
-
if email_address:
|
|
1119
|
-
subject_name.append(x509.NameAttribute(x509.NameOID.EMAIL_ADDRESS, email_address))
|
|
1120
|
-
if country_name:
|
|
1121
|
-
subject_name.append(x509.NameAttribute(x509.NameOID.COUNTRY_NAME, country_name))
|
|
1122
|
-
if state_or_province_name:
|
|
1123
|
-
subject_name.append(
|
|
1124
|
-
x509.NameAttribute(x509.NameOID.STATE_OR_PROVINCE_NAME, state_or_province_name)
|
|
1125
|
-
)
|
|
1126
|
-
if locality_name:
|
|
1127
|
-
subject_name.append(x509.NameAttribute(x509.NameOID.LOCALITY_NAME, locality_name))
|
|
1128
|
-
csr = x509.CertificateSigningRequestBuilder(subject_name=x509.Name(subject_name))
|
|
1129
|
-
|
|
1130
|
-
_sans: List[x509.GeneralName] = []
|
|
1131
|
-
if sans_oid:
|
|
1132
|
-
_sans.extend([x509.RegisteredID(x509.ObjectIdentifier(san)) for san in sans_oid])
|
|
1133
|
-
if sans_ip:
|
|
1134
|
-
_sans.extend([x509.IPAddress(ipaddress.ip_address(san)) for san in sans_ip])
|
|
1135
|
-
if sans:
|
|
1136
|
-
_sans.extend([x509.DNSName(san) for san in sans])
|
|
1137
|
-
if sans_dns:
|
|
1138
|
-
_sans.extend([x509.DNSName(san) for san in sans_dns])
|
|
1139
|
-
if _sans:
|
|
1140
|
-
csr = csr.add_extension(x509.SubjectAlternativeName(set(_sans)), critical=False)
|
|
1141
|
-
|
|
1142
|
-
if additional_critical_extensions:
|
|
1143
|
-
for extension in additional_critical_extensions:
|
|
1144
|
-
csr = csr.add_extension(extension, critical=True)
|
|
1145
|
-
|
|
1146
|
-
signed_certificate = csr.sign(signing_key, hashes.SHA256()) # type: ignore[arg-type]
|
|
1147
|
-
return signed_certificate.public_bytes(serialization.Encoding.PEM)
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
def get_sha256_hex(data: str) -> str:
|
|
1151
|
-
"""Calculate the hash of the provided data and return the hexadecimal representation."""
|
|
1152
|
-
digest = hashes.Hash(hashes.SHA256())
|
|
1153
|
-
digest.update(data.encode())
|
|
1154
|
-
return digest.finalize().hex()
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
def csr_matches_certificate(csr: str, cert: str) -> bool:
|
|
1158
|
-
"""Check if a CSR matches a certificate.
|
|
1159
|
-
|
|
1160
|
-
Args:
|
|
1161
|
-
csr (str): Certificate Signing Request as a string
|
|
1162
|
-
cert (str): Certificate as a string
|
|
1163
|
-
Returns:
|
|
1164
|
-
bool: True/False depending on whether the CSR matches the certificate.
|
|
1165
|
-
"""
|
|
1166
|
-
csr_object = x509.load_pem_x509_csr(csr.encode("utf-8"))
|
|
1167
|
-
cert_object = x509.load_pem_x509_certificate(cert.encode("utf-8"))
|
|
1168
|
-
|
|
1169
|
-
if csr_object.public_key().public_bytes(
|
|
1170
|
-
encoding=serialization.Encoding.PEM,
|
|
1171
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
1172
|
-
) != cert_object.public_key().public_bytes(
|
|
1173
|
-
encoding=serialization.Encoding.PEM,
|
|
1174
|
-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
1175
|
-
):
|
|
1176
|
-
return False
|
|
1177
|
-
return True
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
def _relation_data_is_valid(
|
|
1181
|
-
relation: Relation, app_or_unit: Union[Application, Unit], json_schema: dict
|
|
1182
|
-
) -> bool:
|
|
1183
|
-
"""Check whether relation data is valid based on json schema.
|
|
1184
|
-
|
|
1185
|
-
Args:
|
|
1186
|
-
relation (Relation): Relation object
|
|
1187
|
-
app_or_unit (Union[Application, Unit]): Application or unit object
|
|
1188
|
-
json_schema (dict): Json schema
|
|
1189
|
-
|
|
1190
|
-
Returns:
|
|
1191
|
-
bool: Whether relation data is valid.
|
|
1192
|
-
"""
|
|
1193
|
-
relation_data = _load_relation_data(relation.data[app_or_unit])
|
|
1194
|
-
try:
|
|
1195
|
-
validate(instance=relation_data, schema=json_schema)
|
|
1196
|
-
return True
|
|
1197
|
-
except exceptions.ValidationError:
|
|
1198
|
-
return False
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
class CertificatesProviderCharmEvents(CharmEvents):
|
|
1202
|
-
"""List of events that the TLS Certificates provider charm can leverage."""
|
|
1203
|
-
|
|
1204
|
-
certificate_creation_request = EventSource(CertificateCreationRequestEvent)
|
|
1205
|
-
certificate_revocation_request = EventSource(CertificateRevocationRequestEvent)
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
class CertificatesRequirerCharmEvents(CharmEvents):
|
|
1209
|
-
"""List of events that the TLS Certificates requirer charm can leverage."""
|
|
1210
|
-
|
|
1211
|
-
certificate_available = EventSource(CertificateAvailableEvent)
|
|
1212
|
-
certificate_expiring = EventSource(CertificateExpiringEvent)
|
|
1213
|
-
certificate_invalidated = EventSource(CertificateInvalidatedEvent)
|
|
1214
|
-
all_certificates_invalidated = EventSource(AllCertificatesInvalidatedEvent)
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
class TLSCertificatesProvidesV3(Object):
|
|
1218
|
-
"""TLS certificates provider class to be instantiated by TLS certificates providers."""
|
|
1219
|
-
|
|
1220
|
-
on = CertificatesProviderCharmEvents() # type: ignore[reportAssignmentType]
|
|
1221
|
-
|
|
1222
|
-
def __init__(self, charm: CharmBase, relationship_name: str):
|
|
1223
|
-
super().__init__(charm, relationship_name)
|
|
1224
|
-
self.framework.observe(
|
|
1225
|
-
charm.on[relationship_name].relation_changed, self._on_relation_changed
|
|
1226
|
-
)
|
|
1227
|
-
self.charm = charm
|
|
1228
|
-
self.relationship_name = relationship_name
|
|
1229
|
-
|
|
1230
|
-
def _load_app_relation_data(self, relation: Relation) -> dict:
|
|
1231
|
-
"""Load relation data from the application relation data bag.
|
|
1232
|
-
|
|
1233
|
-
Json loads all data.
|
|
1234
|
-
|
|
1235
|
-
Args:
|
|
1236
|
-
relation: Relation data from the application databag
|
|
1237
|
-
|
|
1238
|
-
Returns:
|
|
1239
|
-
dict: Relation data in dict format.
|
|
1240
|
-
"""
|
|
1241
|
-
# If unit is not leader, it does not try to reach relation data.
|
|
1242
|
-
if not self.model.unit.is_leader():
|
|
1243
|
-
return {}
|
|
1244
|
-
return _load_relation_data(relation.data[self.charm.app])
|
|
1245
|
-
|
|
1246
|
-
def _add_certificate(
|
|
1247
|
-
self,
|
|
1248
|
-
relation_id: int,
|
|
1249
|
-
certificate: str,
|
|
1250
|
-
certificate_signing_request: str,
|
|
1251
|
-
ca: str,
|
|
1252
|
-
chain: List[str],
|
|
1253
|
-
recommended_expiry_notification_time: Optional[int] = None,
|
|
1254
|
-
) -> None:
|
|
1255
|
-
"""Add certificate to relation data.
|
|
1256
|
-
|
|
1257
|
-
Args:
|
|
1258
|
-
relation_id (int): Relation id
|
|
1259
|
-
certificate (str): Certificate
|
|
1260
|
-
certificate_signing_request (str): Certificate Signing Request
|
|
1261
|
-
ca (str): CA Certificate
|
|
1262
|
-
chain (list): CA Chain
|
|
1263
|
-
recommended_expiry_notification_time (int):
|
|
1264
|
-
Time in hours before the certificate expires to notify the user.
|
|
1265
|
-
|
|
1266
|
-
Returns:
|
|
1267
|
-
None
|
|
1268
|
-
"""
|
|
1269
|
-
relation = self.model.get_relation(
|
|
1270
|
-
relation_name=self.relationship_name, relation_id=relation_id
|
|
1271
|
-
)
|
|
1272
|
-
if not relation:
|
|
1273
|
-
raise RuntimeError(
|
|
1274
|
-
f"Relation {self.relationship_name} does not exist - "
|
|
1275
|
-
f"The certificate request can't be completed"
|
|
1276
|
-
)
|
|
1277
|
-
new_certificate = {
|
|
1278
|
-
"certificate": certificate,
|
|
1279
|
-
"certificate_signing_request": certificate_signing_request,
|
|
1280
|
-
"ca": ca,
|
|
1281
|
-
"chain": chain,
|
|
1282
|
-
"recommended_expiry_notification_time": recommended_expiry_notification_time,
|
|
1283
|
-
}
|
|
1284
|
-
provider_relation_data = self._load_app_relation_data(relation)
|
|
1285
|
-
provider_certificates = provider_relation_data.get("certificates", [])
|
|
1286
|
-
certificates = copy.deepcopy(provider_certificates)
|
|
1287
|
-
if new_certificate in certificates:
|
|
1288
|
-
logger.info("Certificate already in relation data - Doing nothing")
|
|
1289
|
-
return
|
|
1290
|
-
if not chain[0] != certificate:
|
|
1291
|
-
logger.warning(
|
|
1292
|
-
"The order of the chain from the TLS Certificates Provider is incorrect. "
|
|
1293
|
-
"The leaf certificate should be the first element of the chain."
|
|
1294
|
-
)
|
|
1295
|
-
elif not chain_has_valid_order(chain):
|
|
1296
|
-
logger.warning(
|
|
1297
|
-
"The order of the chain from the TLS Certificates Provider is partially incorrect."
|
|
1298
|
-
)
|
|
1299
|
-
certificates.append(new_certificate)
|
|
1300
|
-
relation.data[self.model.app]["certificates"] = json.dumps(certificates)
|
|
1301
|
-
|
|
1302
|
-
def _remove_certificate(
|
|
1303
|
-
self,
|
|
1304
|
-
relation_id: int,
|
|
1305
|
-
certificate: Optional[str] = None,
|
|
1306
|
-
certificate_signing_request: Optional[str] = None,
|
|
1307
|
-
) -> None:
|
|
1308
|
-
"""Remove certificate from a given relation based on user provided certificate or csr.
|
|
1309
|
-
|
|
1310
|
-
Args:
|
|
1311
|
-
relation_id (int): Relation id
|
|
1312
|
-
certificate (str): Certificate (optional)
|
|
1313
|
-
certificate_signing_request: Certificate signing request (optional)
|
|
1314
|
-
|
|
1315
|
-
Returns:
|
|
1316
|
-
None
|
|
1317
|
-
"""
|
|
1318
|
-
relation = self.model.get_relation(
|
|
1319
|
-
relation_name=self.relationship_name,
|
|
1320
|
-
relation_id=relation_id,
|
|
1321
|
-
)
|
|
1322
|
-
if not relation:
|
|
1323
|
-
raise RuntimeError(
|
|
1324
|
-
f"Relation {self.relationship_name} with relation id {relation_id} does not exist"
|
|
1325
|
-
)
|
|
1326
|
-
provider_relation_data = self._load_app_relation_data(relation)
|
|
1327
|
-
provider_certificates = provider_relation_data.get("certificates", [])
|
|
1328
|
-
certificates = copy.deepcopy(provider_certificates)
|
|
1329
|
-
for certificate_dict in certificates:
|
|
1330
|
-
if certificate and certificate_dict["certificate"] == certificate:
|
|
1331
|
-
certificates.remove(certificate_dict)
|
|
1332
|
-
if (
|
|
1333
|
-
certificate_signing_request
|
|
1334
|
-
and certificate_dict["certificate_signing_request"] == certificate_signing_request
|
|
1335
|
-
):
|
|
1336
|
-
certificates.remove(certificate_dict)
|
|
1337
|
-
relation.data[self.model.app]["certificates"] = json.dumps(certificates)
|
|
1338
|
-
|
|
1339
|
-
def revoke_all_certificates(self) -> None:
|
|
1340
|
-
"""Revoke all certificates of this provider.
|
|
1341
|
-
|
|
1342
|
-
This method is meant to be used when the Root CA has changed.
|
|
1343
|
-
"""
|
|
1344
|
-
for relation in self.model.relations[self.relationship_name]:
|
|
1345
|
-
provider_relation_data = self._load_app_relation_data(relation)
|
|
1346
|
-
provider_certificates = copy.deepcopy(provider_relation_data.get("certificates", []))
|
|
1347
|
-
for certificate in provider_certificates:
|
|
1348
|
-
certificate["revoked"] = True
|
|
1349
|
-
relation.data[self.model.app]["certificates"] = json.dumps(provider_certificates)
|
|
1350
|
-
|
|
1351
|
-
def set_relation_certificate(
|
|
1352
|
-
self,
|
|
1353
|
-
certificate: str,
|
|
1354
|
-
certificate_signing_request: str,
|
|
1355
|
-
ca: str,
|
|
1356
|
-
chain: List[str],
|
|
1357
|
-
relation_id: int,
|
|
1358
|
-
recommended_expiry_notification_time: Optional[int] = None,
|
|
1359
|
-
) -> None:
|
|
1360
|
-
"""Add certificates to relation data.
|
|
1361
|
-
|
|
1362
|
-
Args:
|
|
1363
|
-
certificate (str): Certificate
|
|
1364
|
-
certificate_signing_request (str): Certificate signing request
|
|
1365
|
-
ca (str): CA Certificate
|
|
1366
|
-
chain (list): CA Chain
|
|
1367
|
-
relation_id (int): Juju relation ID
|
|
1368
|
-
recommended_expiry_notification_time (int):
|
|
1369
|
-
Recommended time in hours before the certificate expires to notify the user.
|
|
1370
|
-
|
|
1371
|
-
Returns:
|
|
1372
|
-
None
|
|
1373
|
-
"""
|
|
1374
|
-
if not self.model.unit.is_leader():
|
|
1375
|
-
return
|
|
1376
|
-
certificates_relation = self.model.get_relation(
|
|
1377
|
-
relation_name=self.relationship_name, relation_id=relation_id
|
|
1378
|
-
)
|
|
1379
|
-
if not certificates_relation:
|
|
1380
|
-
raise RuntimeError(f"Relation {self.relationship_name} does not exist")
|
|
1381
|
-
self._remove_certificate(
|
|
1382
|
-
certificate_signing_request=certificate_signing_request.strip(),
|
|
1383
|
-
relation_id=relation_id,
|
|
1384
|
-
)
|
|
1385
|
-
self._add_certificate(
|
|
1386
|
-
relation_id=relation_id,
|
|
1387
|
-
certificate=certificate.strip(),
|
|
1388
|
-
certificate_signing_request=certificate_signing_request.strip(),
|
|
1389
|
-
ca=ca.strip(),
|
|
1390
|
-
chain=[cert.strip() for cert in chain],
|
|
1391
|
-
recommended_expiry_notification_time=recommended_expiry_notification_time,
|
|
1392
|
-
)
|
|
1393
|
-
|
|
1394
|
-
def remove_certificate(self, certificate: str) -> None:
|
|
1395
|
-
"""Remove a given certificate from relation data.
|
|
1396
|
-
|
|
1397
|
-
Args:
|
|
1398
|
-
certificate (str): TLS Certificate
|
|
1399
|
-
|
|
1400
|
-
Returns:
|
|
1401
|
-
None
|
|
1402
|
-
"""
|
|
1403
|
-
certificates_relation = self.model.relations[self.relationship_name]
|
|
1404
|
-
if not certificates_relation:
|
|
1405
|
-
raise RuntimeError(f"Relation {self.relationship_name} does not exist")
|
|
1406
|
-
for certificate_relation in certificates_relation:
|
|
1407
|
-
self._remove_certificate(certificate=certificate, relation_id=certificate_relation.id)
|
|
1408
|
-
|
|
1409
|
-
def get_issued_certificates(
|
|
1410
|
-
self, relation_id: Optional[int] = None
|
|
1411
|
-
) -> List[ProviderCertificate]:
|
|
1412
|
-
"""Return a List of issued (non revoked) certificates.
|
|
1413
|
-
|
|
1414
|
-
Returns:
|
|
1415
|
-
List: List of ProviderCertificate objects
|
|
1416
|
-
"""
|
|
1417
|
-
provider_certificates = self.get_provider_certificates(relation_id=relation_id)
|
|
1418
|
-
return [certificate for certificate in provider_certificates if not certificate.revoked]
|
|
1419
|
-
|
|
1420
|
-
def get_provider_certificates(
|
|
1421
|
-
self, relation_id: Optional[int] = None
|
|
1422
|
-
) -> List[ProviderCertificate]:
|
|
1423
|
-
"""Return a List of issued certificates.
|
|
1424
|
-
|
|
1425
|
-
Returns:
|
|
1426
|
-
List: List of ProviderCertificate objects
|
|
1427
|
-
"""
|
|
1428
|
-
certificates: List[ProviderCertificate] = []
|
|
1429
|
-
relations = (
|
|
1430
|
-
[
|
|
1431
|
-
relation
|
|
1432
|
-
for relation in self.model.relations[self.relationship_name]
|
|
1433
|
-
if relation.id == relation_id
|
|
1434
|
-
]
|
|
1435
|
-
if relation_id is not None
|
|
1436
|
-
else self.model.relations.get(self.relationship_name, [])
|
|
1437
|
-
)
|
|
1438
|
-
for relation in relations:
|
|
1439
|
-
if not relation.app:
|
|
1440
|
-
logger.warning("Relation %s does not have an application", relation.id)
|
|
1441
|
-
continue
|
|
1442
|
-
provider_relation_data = self._load_app_relation_data(relation)
|
|
1443
|
-
provider_certificates = provider_relation_data.get("certificates", [])
|
|
1444
|
-
for certificate in provider_certificates:
|
|
1445
|
-
try:
|
|
1446
|
-
certificate_object = x509.load_pem_x509_certificate(
|
|
1447
|
-
data=certificate["certificate"].encode()
|
|
1448
|
-
)
|
|
1449
|
-
except ValueError as e:
|
|
1450
|
-
logger.error("Could not load certificate - Skipping: %s", e)
|
|
1451
|
-
continue
|
|
1452
|
-
provider_certificate = ProviderCertificate(
|
|
1453
|
-
relation_id=relation.id,
|
|
1454
|
-
application_name=relation.app.name,
|
|
1455
|
-
csr=certificate["certificate_signing_request"],
|
|
1456
|
-
certificate=certificate["certificate"],
|
|
1457
|
-
ca=certificate["ca"],
|
|
1458
|
-
chain=certificate["chain"],
|
|
1459
|
-
revoked=certificate.get("revoked", False),
|
|
1460
|
-
expiry_time=certificate_object.not_valid_after_utc,
|
|
1461
|
-
expiry_notification_time=certificate.get(
|
|
1462
|
-
"recommended_expiry_notification_time"
|
|
1463
|
-
),
|
|
1464
|
-
)
|
|
1465
|
-
certificates.append(provider_certificate)
|
|
1466
|
-
return certificates
|
|
1467
|
-
|
|
1468
|
-
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
1469
|
-
"""Handle relation changed event.
|
|
1470
|
-
|
|
1471
|
-
Looks at the relation data and either emits:
|
|
1472
|
-
- certificate request event: If the unit relation data contains a CSR for which
|
|
1473
|
-
a certificate does not exist in the provider relation data.
|
|
1474
|
-
- certificate revocation event: If the provider relation data contains a CSR for which
|
|
1475
|
-
a csr does not exist in the requirer relation data.
|
|
1476
|
-
|
|
1477
|
-
Args:
|
|
1478
|
-
event: Juju event
|
|
1479
|
-
|
|
1480
|
-
Returns:
|
|
1481
|
-
None
|
|
1482
|
-
"""
|
|
1483
|
-
if event.unit is None:
|
|
1484
|
-
logger.error("Relation_changed event does not have a unit.")
|
|
1485
|
-
return
|
|
1486
|
-
if not self.model.unit.is_leader():
|
|
1487
|
-
return
|
|
1488
|
-
if not _relation_data_is_valid(event.relation, event.unit, REQUIRER_JSON_SCHEMA):
|
|
1489
|
-
logger.debug("Relation data did not pass JSON Schema validation")
|
|
1490
|
-
return
|
|
1491
|
-
provider_certificates = self.get_provider_certificates(relation_id=event.relation.id)
|
|
1492
|
-
requirer_csrs = self.get_requirer_csrs(relation_id=event.relation.id)
|
|
1493
|
-
provider_csrs = [
|
|
1494
|
-
certificate_creation_request.csr
|
|
1495
|
-
for certificate_creation_request in provider_certificates
|
|
1496
|
-
]
|
|
1497
|
-
for certificate_request in requirer_csrs:
|
|
1498
|
-
if certificate_request.csr not in provider_csrs:
|
|
1499
|
-
self.on.certificate_creation_request.emit(
|
|
1500
|
-
certificate_signing_request=certificate_request.csr,
|
|
1501
|
-
relation_id=certificate_request.relation_id,
|
|
1502
|
-
is_ca=certificate_request.is_ca,
|
|
1503
|
-
)
|
|
1504
|
-
self._revoke_certificates_for_which_no_csr_exists(relation_id=event.relation.id)
|
|
1505
|
-
|
|
1506
|
-
def _revoke_certificates_for_which_no_csr_exists(self, relation_id: int) -> None:
|
|
1507
|
-
"""Revoke certificates for which no unit has a CSR.
|
|
1508
|
-
|
|
1509
|
-
Goes through all generated certificates and compare against the list of CSRs for all units.
|
|
1510
|
-
|
|
1511
|
-
Returns:
|
|
1512
|
-
None
|
|
1513
|
-
"""
|
|
1514
|
-
provider_certificates = self.get_unsolicited_certificates(relation_id=relation_id)
|
|
1515
|
-
for provider_certificate in provider_certificates:
|
|
1516
|
-
self.on.certificate_revocation_request.emit(
|
|
1517
|
-
certificate=provider_certificate.certificate,
|
|
1518
|
-
certificate_signing_request=provider_certificate.csr,
|
|
1519
|
-
ca=provider_certificate.ca,
|
|
1520
|
-
chain=provider_certificate.chain,
|
|
1521
|
-
)
|
|
1522
|
-
self.remove_certificate(certificate=provider_certificate.certificate)
|
|
1523
|
-
|
|
1524
|
-
def get_unsolicited_certificates(
|
|
1525
|
-
self, relation_id: Optional[int] = None
|
|
1526
|
-
) -> List[ProviderCertificate]:
|
|
1527
|
-
"""Return provider certificates for which no certificate requests exists.
|
|
1528
|
-
|
|
1529
|
-
Those certificates should be revoked.
|
|
1530
|
-
"""
|
|
1531
|
-
unsolicited_certificates: List[ProviderCertificate] = []
|
|
1532
|
-
provider_certificates = self.get_provider_certificates(relation_id=relation_id)
|
|
1533
|
-
requirer_csrs = self.get_requirer_csrs(relation_id=relation_id)
|
|
1534
|
-
list_of_csrs = [csr.csr for csr in requirer_csrs]
|
|
1535
|
-
for certificate in provider_certificates:
|
|
1536
|
-
if certificate.csr not in list_of_csrs:
|
|
1537
|
-
unsolicited_certificates.append(certificate)
|
|
1538
|
-
return unsolicited_certificates
|
|
1539
|
-
|
|
1540
|
-
def get_outstanding_certificate_requests(
|
|
1541
|
-
self, relation_id: Optional[int] = None
|
|
1542
|
-
) -> List[RequirerCSR]:
|
|
1543
|
-
"""Return CSR's for which no certificate has been issued.
|
|
1544
|
-
|
|
1545
|
-
Args:
|
|
1546
|
-
relation_id (int): Relation id
|
|
1547
|
-
|
|
1548
|
-
Returns:
|
|
1549
|
-
list: List of RequirerCSR objects.
|
|
1550
|
-
"""
|
|
1551
|
-
requirer_csrs = self.get_requirer_csrs(relation_id=relation_id)
|
|
1552
|
-
outstanding_csrs: List[RequirerCSR] = []
|
|
1553
|
-
for relation_csr in requirer_csrs:
|
|
1554
|
-
if not self.certificate_issued_for_csr(
|
|
1555
|
-
app_name=relation_csr.application_name,
|
|
1556
|
-
csr=relation_csr.csr,
|
|
1557
|
-
relation_id=relation_id,
|
|
1558
|
-
):
|
|
1559
|
-
outstanding_csrs.append(relation_csr)
|
|
1560
|
-
return outstanding_csrs
|
|
1561
|
-
|
|
1562
|
-
def get_requirer_csrs(self, relation_id: Optional[int] = None) -> List[RequirerCSR]:
|
|
1563
|
-
"""Return a list of requirers' CSRs.
|
|
1564
|
-
|
|
1565
|
-
It returns CSRs from all relations if relation_id is not specified.
|
|
1566
|
-
CSRs are returned per relation id, application name and unit name.
|
|
1567
|
-
|
|
1568
|
-
Returns:
|
|
1569
|
-
list: List[RequirerCSR]
|
|
1570
|
-
"""
|
|
1571
|
-
relation_csrs: List[RequirerCSR] = []
|
|
1572
|
-
relations = (
|
|
1573
|
-
[
|
|
1574
|
-
relation
|
|
1575
|
-
for relation in self.model.relations[self.relationship_name]
|
|
1576
|
-
if relation.id == relation_id
|
|
1577
|
-
]
|
|
1578
|
-
if relation_id is not None
|
|
1579
|
-
else self.model.relations.get(self.relationship_name, [])
|
|
1580
|
-
)
|
|
1581
|
-
|
|
1582
|
-
for relation in relations:
|
|
1583
|
-
for unit in relation.units:
|
|
1584
|
-
requirer_relation_data = _load_relation_data(relation.data[unit])
|
|
1585
|
-
unit_csrs_list = requirer_relation_data.get("certificate_signing_requests", [])
|
|
1586
|
-
for unit_csr in unit_csrs_list:
|
|
1587
|
-
csr = unit_csr.get("certificate_signing_request")
|
|
1588
|
-
if not csr:
|
|
1589
|
-
logger.warning("No CSR found in relation data - Skipping")
|
|
1590
|
-
continue
|
|
1591
|
-
ca = unit_csr.get("ca", False)
|
|
1592
|
-
if not relation.app:
|
|
1593
|
-
logger.warning("No remote app in relation - Skipping")
|
|
1594
|
-
continue
|
|
1595
|
-
relation_csr = RequirerCSR(
|
|
1596
|
-
relation_id=relation.id,
|
|
1597
|
-
application_name=relation.app.name,
|
|
1598
|
-
unit_name=unit.name,
|
|
1599
|
-
csr=csr,
|
|
1600
|
-
is_ca=ca,
|
|
1601
|
-
)
|
|
1602
|
-
relation_csrs.append(relation_csr)
|
|
1603
|
-
return relation_csrs
|
|
1604
|
-
|
|
1605
|
-
def certificate_issued_for_csr(
|
|
1606
|
-
self, app_name: str, csr: str, relation_id: Optional[int]
|
|
1607
|
-
) -> bool:
|
|
1608
|
-
"""Check whether a certificate has been issued for a given CSR.
|
|
1609
|
-
|
|
1610
|
-
Args:
|
|
1611
|
-
app_name (str): Application name that the CSR belongs to.
|
|
1612
|
-
csr (str): Certificate Signing Request.
|
|
1613
|
-
relation_id (Optional[int]): Relation ID
|
|
1614
|
-
|
|
1615
|
-
Returns:
|
|
1616
|
-
bool: True/False depending on whether a certificate has been issued for the given CSR.
|
|
1617
|
-
"""
|
|
1618
|
-
issued_certificates_per_csr = self.get_issued_certificates(relation_id=relation_id)
|
|
1619
|
-
for issued_certificate in issued_certificates_per_csr:
|
|
1620
|
-
if issued_certificate.csr == csr and issued_certificate.application_name == app_name:
|
|
1621
|
-
return csr_matches_certificate(csr, issued_certificate.certificate)
|
|
1622
|
-
return False
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
class TLSCertificatesRequiresV3(Object):
|
|
1626
|
-
"""TLS certificates requirer class to be instantiated by TLS certificates requirers."""
|
|
1627
|
-
|
|
1628
|
-
on = CertificatesRequirerCharmEvents() # type: ignore[reportAssignmentType]
|
|
1629
|
-
|
|
1630
|
-
def __init__(
|
|
1631
|
-
self,
|
|
1632
|
-
charm: CharmBase,
|
|
1633
|
-
relationship_name: str,
|
|
1634
|
-
expiry_notification_time: Optional[int] = None,
|
|
1635
|
-
):
|
|
1636
|
-
"""Generate/use private key and observes relation changed event.
|
|
1637
|
-
|
|
1638
|
-
Args:
|
|
1639
|
-
charm: Charm object
|
|
1640
|
-
relationship_name: Juju relation name
|
|
1641
|
-
expiry_notification_time (int): Number of hours prior to certificate expiry.
|
|
1642
|
-
Used to trigger the CertificateExpiring event.
|
|
1643
|
-
This value is used as a recommendation only,
|
|
1644
|
-
The actual value is calculated taking into account the provider's recommendation.
|
|
1645
|
-
"""
|
|
1646
|
-
super().__init__(charm, relationship_name)
|
|
1647
|
-
if not JujuVersion.from_environ().has_secrets:
|
|
1648
|
-
logger.warning("This version of the TLS library requires Juju secrets (Juju >= 3.0)")
|
|
1649
|
-
self.relationship_name = relationship_name
|
|
1650
|
-
self.charm = charm
|
|
1651
|
-
self.expiry_notification_time = expiry_notification_time
|
|
1652
|
-
self.framework.observe(
|
|
1653
|
-
charm.on[relationship_name].relation_changed, self._on_relation_changed
|
|
1654
|
-
)
|
|
1655
|
-
self.framework.observe(
|
|
1656
|
-
charm.on[relationship_name].relation_broken, self._on_relation_broken
|
|
1657
|
-
)
|
|
1658
|
-
self.framework.observe(charm.on.secret_expired, self._on_secret_expired)
|
|
1659
|
-
|
|
1660
|
-
def get_requirer_csrs(self) -> List[RequirerCSR]:
|
|
1661
|
-
"""Return list of requirer's CSRs from relation unit data.
|
|
1662
|
-
|
|
1663
|
-
Returns:
|
|
1664
|
-
list: List of RequirerCSR objects.
|
|
1665
|
-
"""
|
|
1666
|
-
relation = self.model.get_relation(self.relationship_name)
|
|
1667
|
-
if not relation:
|
|
1668
|
-
return []
|
|
1669
|
-
requirer_csrs = []
|
|
1670
|
-
requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
|
|
1671
|
-
requirer_csrs_dict = requirer_relation_data.get("certificate_signing_requests", [])
|
|
1672
|
-
for requirer_csr_dict in requirer_csrs_dict:
|
|
1673
|
-
csr = requirer_csr_dict.get("certificate_signing_request")
|
|
1674
|
-
if not csr:
|
|
1675
|
-
logger.warning("No CSR found in relation data - Skipping")
|
|
1676
|
-
continue
|
|
1677
|
-
ca = requirer_csr_dict.get("ca", False)
|
|
1678
|
-
relation_csr = RequirerCSR(
|
|
1679
|
-
relation_id=relation.id,
|
|
1680
|
-
application_name=self.model.app.name,
|
|
1681
|
-
unit_name=self.model.unit.name,
|
|
1682
|
-
csr=csr,
|
|
1683
|
-
is_ca=ca,
|
|
1684
|
-
)
|
|
1685
|
-
requirer_csrs.append(relation_csr)
|
|
1686
|
-
return requirer_csrs
|
|
1687
|
-
|
|
1688
|
-
def get_provider_certificates(self) -> List[ProviderCertificate]:
|
|
1689
|
-
"""Return list of certificates from the provider's relation data."""
|
|
1690
|
-
provider_certificates: List[ProviderCertificate] = []
|
|
1691
|
-
relation = self.model.get_relation(self.relationship_name)
|
|
1692
|
-
if not relation:
|
|
1693
|
-
logger.debug("No relation: %s", self.relationship_name)
|
|
1694
|
-
return []
|
|
1695
|
-
if not relation.app:
|
|
1696
|
-
logger.debug("No remote app in relation: %s", self.relationship_name)
|
|
1697
|
-
return []
|
|
1698
|
-
provider_relation_data = _load_relation_data(relation.data[relation.app])
|
|
1699
|
-
provider_certificate_dicts = provider_relation_data.get("certificates", [])
|
|
1700
|
-
for provider_certificate_dict in provider_certificate_dicts:
|
|
1701
|
-
certificate = provider_certificate_dict.get("certificate")
|
|
1702
|
-
if not certificate:
|
|
1703
|
-
logger.warning("No certificate found in relation data - Skipping")
|
|
1704
|
-
continue
|
|
1705
|
-
try:
|
|
1706
|
-
certificate_object = x509.load_pem_x509_certificate(data=certificate.encode())
|
|
1707
|
-
except ValueError as e:
|
|
1708
|
-
logger.error("Could not load certificate - Skipping: %s", e)
|
|
1709
|
-
continue
|
|
1710
|
-
ca = provider_certificate_dict.get("ca")
|
|
1711
|
-
chain = provider_certificate_dict.get("chain", [])
|
|
1712
|
-
csr = provider_certificate_dict.get("certificate_signing_request")
|
|
1713
|
-
recommended_expiry_notification_time = provider_certificate_dict.get(
|
|
1714
|
-
"recommended_expiry_notification_time"
|
|
1715
|
-
)
|
|
1716
|
-
expiry_time = certificate_object.not_valid_after_utc
|
|
1717
|
-
validity_start_time = certificate_object.not_valid_before_utc
|
|
1718
|
-
expiry_notification_time = calculate_expiry_notification_time(
|
|
1719
|
-
validity_start_time=validity_start_time,
|
|
1720
|
-
expiry_time=expiry_time,
|
|
1721
|
-
provider_recommended_notification_time=recommended_expiry_notification_time,
|
|
1722
|
-
requirer_recommended_notification_time=self.expiry_notification_time,
|
|
1723
|
-
)
|
|
1724
|
-
if not csr:
|
|
1725
|
-
logger.warning("No CSR found in relation data - Skipping")
|
|
1726
|
-
continue
|
|
1727
|
-
revoked = provider_certificate_dict.get("revoked", False)
|
|
1728
|
-
provider_certificate = ProviderCertificate(
|
|
1729
|
-
relation_id=relation.id,
|
|
1730
|
-
application_name=relation.app.name,
|
|
1731
|
-
csr=csr,
|
|
1732
|
-
certificate=certificate,
|
|
1733
|
-
ca=ca,
|
|
1734
|
-
chain=chain,
|
|
1735
|
-
revoked=revoked,
|
|
1736
|
-
expiry_time=expiry_time,
|
|
1737
|
-
expiry_notification_time=expiry_notification_time,
|
|
1738
|
-
)
|
|
1739
|
-
provider_certificates.append(provider_certificate)
|
|
1740
|
-
return provider_certificates
|
|
1741
|
-
|
|
1742
|
-
def _add_requirer_csr_to_relation_data(self, csr: str, is_ca: bool) -> None:
|
|
1743
|
-
"""Add CSR to relation data.
|
|
1744
|
-
|
|
1745
|
-
Args:
|
|
1746
|
-
csr (str): Certificate Signing Request
|
|
1747
|
-
is_ca (bool): Whether the certificate is a CA certificate
|
|
1748
|
-
|
|
1749
|
-
Returns:
|
|
1750
|
-
None
|
|
1751
|
-
"""
|
|
1752
|
-
relation = self.model.get_relation(self.relationship_name)
|
|
1753
|
-
if not relation:
|
|
1754
|
-
raise RuntimeError(
|
|
1755
|
-
f"Relation {self.relationship_name} does not exist - "
|
|
1756
|
-
f"The certificate request can't be completed"
|
|
1757
|
-
)
|
|
1758
|
-
for requirer_csr in self.get_requirer_csrs():
|
|
1759
|
-
if requirer_csr.csr == csr and requirer_csr.is_ca == is_ca:
|
|
1760
|
-
logger.info("CSR already in relation data - Doing nothing")
|
|
1761
|
-
return
|
|
1762
|
-
new_csr_dict = {
|
|
1763
|
-
"certificate_signing_request": csr,
|
|
1764
|
-
"ca": is_ca,
|
|
1765
|
-
}
|
|
1766
|
-
requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
|
|
1767
|
-
existing_relation_data = requirer_relation_data.get("certificate_signing_requests", [])
|
|
1768
|
-
new_relation_data = copy.deepcopy(existing_relation_data)
|
|
1769
|
-
new_relation_data.append(new_csr_dict)
|
|
1770
|
-
relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(
|
|
1771
|
-
new_relation_data
|
|
1772
|
-
)
|
|
1773
|
-
|
|
1774
|
-
def _remove_requirer_csr_from_relation_data(self, csr: str) -> None:
|
|
1775
|
-
"""Remove CSR from relation data.
|
|
1776
|
-
|
|
1777
|
-
Args:
|
|
1778
|
-
csr (str): Certificate signing request
|
|
1779
|
-
|
|
1780
|
-
Returns:
|
|
1781
|
-
None
|
|
1782
|
-
"""
|
|
1783
|
-
relation = self.model.get_relation(self.relationship_name)
|
|
1784
|
-
if not relation:
|
|
1785
|
-
raise RuntimeError(
|
|
1786
|
-
f"Relation {self.relationship_name} does not exist - "
|
|
1787
|
-
f"The certificate request can't be completed"
|
|
1788
|
-
)
|
|
1789
|
-
if not self.get_requirer_csrs():
|
|
1790
|
-
logger.info("No CSRs in relation data - Doing nothing")
|
|
1791
|
-
return
|
|
1792
|
-
requirer_relation_data = _load_relation_data(relation.data[self.model.unit])
|
|
1793
|
-
existing_relation_data = requirer_relation_data.get("certificate_signing_requests", [])
|
|
1794
|
-
new_relation_data = copy.deepcopy(existing_relation_data)
|
|
1795
|
-
for requirer_csr in new_relation_data:
|
|
1796
|
-
if requirer_csr["certificate_signing_request"] == csr:
|
|
1797
|
-
new_relation_data.remove(requirer_csr)
|
|
1798
|
-
relation.data[self.model.unit]["certificate_signing_requests"] = json.dumps(
|
|
1799
|
-
new_relation_data
|
|
1800
|
-
)
|
|
1801
|
-
|
|
1802
|
-
def request_certificate_creation(
|
|
1803
|
-
self, certificate_signing_request: bytes, is_ca: bool = False
|
|
1804
|
-
) -> None:
|
|
1805
|
-
"""Request TLS certificate to provider charm.
|
|
1806
|
-
|
|
1807
|
-
Args:
|
|
1808
|
-
certificate_signing_request (bytes): Certificate Signing Request
|
|
1809
|
-
is_ca (bool): Whether the certificate is a CA certificate
|
|
1810
|
-
|
|
1811
|
-
Returns:
|
|
1812
|
-
None
|
|
1813
|
-
"""
|
|
1814
|
-
relation = self.model.get_relation(self.relationship_name)
|
|
1815
|
-
if not relation:
|
|
1816
|
-
raise RuntimeError(
|
|
1817
|
-
f"Relation {self.relationship_name} does not exist - "
|
|
1818
|
-
f"The certificate request can't be completed"
|
|
1819
|
-
)
|
|
1820
|
-
self._add_requirer_csr_to_relation_data(
|
|
1821
|
-
certificate_signing_request.decode().strip(), is_ca=is_ca
|
|
1822
|
-
)
|
|
1823
|
-
logger.info("Certificate request sent to provider")
|
|
1824
|
-
|
|
1825
|
-
def request_certificate_revocation(self, certificate_signing_request: bytes) -> None:
|
|
1826
|
-
"""Remove CSR from relation data.
|
|
1827
|
-
|
|
1828
|
-
The provider of this relation is then expected to remove certificates associated to this
|
|
1829
|
-
CSR from the relation data as well and emit a request_certificate_revocation event for the
|
|
1830
|
-
provider charm to interpret.
|
|
1831
|
-
|
|
1832
|
-
Args:
|
|
1833
|
-
certificate_signing_request (bytes): Certificate Signing Request
|
|
1834
|
-
|
|
1835
|
-
Returns:
|
|
1836
|
-
None
|
|
1837
|
-
"""
|
|
1838
|
-
self._remove_requirer_csr_from_relation_data(certificate_signing_request.decode().strip())
|
|
1839
|
-
logger.info("Certificate revocation sent to provider")
|
|
1840
|
-
|
|
1841
|
-
def request_certificate_renewal(
|
|
1842
|
-
self, old_certificate_signing_request: bytes, new_certificate_signing_request: bytes
|
|
1843
|
-
) -> None:
|
|
1844
|
-
"""Renew certificate.
|
|
1845
|
-
|
|
1846
|
-
Removes old CSR from relation data and adds new one.
|
|
1847
|
-
|
|
1848
|
-
Args:
|
|
1849
|
-
old_certificate_signing_request: Old CSR
|
|
1850
|
-
new_certificate_signing_request: New CSR
|
|
1851
|
-
|
|
1852
|
-
Returns:
|
|
1853
|
-
None
|
|
1854
|
-
"""
|
|
1855
|
-
try:
|
|
1856
|
-
self.request_certificate_revocation(
|
|
1857
|
-
certificate_signing_request=old_certificate_signing_request
|
|
1858
|
-
)
|
|
1859
|
-
except RuntimeError:
|
|
1860
|
-
logger.warning("Certificate revocation failed.")
|
|
1861
|
-
self.request_certificate_creation(
|
|
1862
|
-
certificate_signing_request=new_certificate_signing_request
|
|
1863
|
-
)
|
|
1864
|
-
logger.info("Certificate renewal request completed.")
|
|
1865
|
-
|
|
1866
|
-
def get_assigned_certificates(self) -> List[ProviderCertificate]:
|
|
1867
|
-
"""Get a list of certificates that were assigned to this unit.
|
|
1868
|
-
|
|
1869
|
-
Returns:
|
|
1870
|
-
List: List[ProviderCertificate]
|
|
1871
|
-
"""
|
|
1872
|
-
assigned_certificates = []
|
|
1873
|
-
for requirer_csr in self.get_certificate_signing_requests(fulfilled_only=True):
|
|
1874
|
-
if cert := self._find_certificate_in_relation_data(requirer_csr.csr):
|
|
1875
|
-
assigned_certificates.append(cert)
|
|
1876
|
-
return assigned_certificates
|
|
1877
|
-
|
|
1878
|
-
def get_expiring_certificates(self) -> List[ProviderCertificate]:
|
|
1879
|
-
"""Get a list of certificates that were assigned to this unit that are expiring or expired.
|
|
1880
|
-
|
|
1881
|
-
Returns:
|
|
1882
|
-
List: List[ProviderCertificate]
|
|
1883
|
-
"""
|
|
1884
|
-
expiring_certificates: List[ProviderCertificate] = []
|
|
1885
|
-
for requirer_csr in self.get_certificate_signing_requests(fulfilled_only=True):
|
|
1886
|
-
if cert := self._find_certificate_in_relation_data(requirer_csr.csr):
|
|
1887
|
-
if not cert.expiry_time or not cert.expiry_notification_time:
|
|
1888
|
-
continue
|
|
1889
|
-
if datetime.now(timezone.utc) > cert.expiry_notification_time:
|
|
1890
|
-
expiring_certificates.append(cert)
|
|
1891
|
-
return expiring_certificates
|
|
1892
|
-
|
|
1893
|
-
def get_certificate_signing_requests(
|
|
1894
|
-
self,
|
|
1895
|
-
fulfilled_only: bool = False,
|
|
1896
|
-
unfulfilled_only: bool = False,
|
|
1897
|
-
) -> List[RequirerCSR]:
|
|
1898
|
-
"""Get the list of CSR's that were sent to the provider.
|
|
1899
|
-
|
|
1900
|
-
You can choose to get only the CSR's that have a certificate assigned or only the CSR's
|
|
1901
|
-
that don't.
|
|
1902
|
-
|
|
1903
|
-
Args:
|
|
1904
|
-
fulfilled_only (bool): This option will discard CSRs that don't have certificates yet.
|
|
1905
|
-
unfulfilled_only (bool): This option will discard CSRs that have certificates signed.
|
|
1906
|
-
|
|
1907
|
-
Returns:
|
|
1908
|
-
List of RequirerCSR objects.
|
|
1909
|
-
"""
|
|
1910
|
-
csrs = []
|
|
1911
|
-
for requirer_csr in self.get_requirer_csrs():
|
|
1912
|
-
cert = self._find_certificate_in_relation_data(requirer_csr.csr)
|
|
1913
|
-
if (unfulfilled_only and cert) or (fulfilled_only and not cert):
|
|
1914
|
-
continue
|
|
1915
|
-
csrs.append(requirer_csr)
|
|
1916
|
-
|
|
1917
|
-
return csrs
|
|
1918
|
-
|
|
1919
|
-
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
1920
|
-
"""Handle relation changed event.
|
|
1921
|
-
|
|
1922
|
-
Goes through all providers certificates that match a requested CSR.
|
|
1923
|
-
|
|
1924
|
-
If the provider certificate is revoked, emit a CertificateInvalidateEvent,
|
|
1925
|
-
otherwise emit a CertificateAvailableEvent.
|
|
1926
|
-
|
|
1927
|
-
Remove the secret for revoked certificate, or add a secret with the correct expiry
|
|
1928
|
-
time for new certificates.
|
|
1929
|
-
|
|
1930
|
-
Args:
|
|
1931
|
-
event: Juju event
|
|
1932
|
-
|
|
1933
|
-
Returns:
|
|
1934
|
-
None
|
|
1935
|
-
"""
|
|
1936
|
-
if not event.app:
|
|
1937
|
-
logger.warning("No remote app in relation - Skipping")
|
|
1938
|
-
return
|
|
1939
|
-
if not _relation_data_is_valid(event.relation, event.app, PROVIDER_JSON_SCHEMA):
|
|
1940
|
-
logger.debug("Relation data did not pass JSON Schema validation")
|
|
1941
|
-
return
|
|
1942
|
-
provider_certificates = self.get_provider_certificates()
|
|
1943
|
-
requirer_csrs = [
|
|
1944
|
-
certificate_creation_request.csr
|
|
1945
|
-
for certificate_creation_request in self.get_requirer_csrs()
|
|
1946
|
-
]
|
|
1947
|
-
for certificate in provider_certificates:
|
|
1948
|
-
if certificate.csr in requirer_csrs:
|
|
1949
|
-
csr_in_sha256_hex = get_sha256_hex(certificate.csr)
|
|
1950
|
-
if certificate.revoked:
|
|
1951
|
-
with suppress(SecretNotFoundError):
|
|
1952
|
-
logger.debug(
|
|
1953
|
-
"Removing secret with label %s",
|
|
1954
|
-
f"{LIBID}-{csr_in_sha256_hex}",
|
|
1955
|
-
)
|
|
1956
|
-
secret = self.model.get_secret(label=f"{LIBID}-{csr_in_sha256_hex}")
|
|
1957
|
-
secret.remove_all_revisions()
|
|
1958
|
-
self.on.certificate_invalidated.emit(
|
|
1959
|
-
reason="revoked",
|
|
1960
|
-
certificate=certificate.certificate,
|
|
1961
|
-
certificate_signing_request=certificate.csr,
|
|
1962
|
-
ca=certificate.ca,
|
|
1963
|
-
chain=certificate.chain,
|
|
1964
|
-
)
|
|
1965
|
-
else:
|
|
1966
|
-
try:
|
|
1967
|
-
secret = self.model.get_secret(label=f"{LIBID}-{csr_in_sha256_hex}")
|
|
1968
|
-
logger.debug(
|
|
1969
|
-
"Setting secret with label %s", f"{LIBID}-{csr_in_sha256_hex}"
|
|
1970
|
-
)
|
|
1971
|
-
# Juju < 3.6 will create a new revision even if the content is the same
|
|
1972
|
-
if (
|
|
1973
|
-
secret.get_content(refresh=True).get("certificate", "")
|
|
1974
|
-
== certificate.certificate
|
|
1975
|
-
):
|
|
1976
|
-
logger.debug(
|
|
1977
|
-
"Secret %s with correct certificate already exists",
|
|
1978
|
-
f"{LIBID}-{csr_in_sha256_hex}",
|
|
1979
|
-
)
|
|
1980
|
-
continue
|
|
1981
|
-
secret.set_content(
|
|
1982
|
-
{"certificate": certificate.certificate, "csr": certificate.csr}
|
|
1983
|
-
)
|
|
1984
|
-
secret.set_info(
|
|
1985
|
-
expire=self._get_next_secret_expiry_time(certificate),
|
|
1986
|
-
)
|
|
1987
|
-
except SecretNotFoundError:
|
|
1988
|
-
logger.debug(
|
|
1989
|
-
"Creating new secret with label %s", f"{LIBID}-{csr_in_sha256_hex}"
|
|
1990
|
-
)
|
|
1991
|
-
secret = self.charm.unit.add_secret(
|
|
1992
|
-
{"certificate": certificate.certificate, "csr": certificate.csr},
|
|
1993
|
-
label=f"{LIBID}-{csr_in_sha256_hex}",
|
|
1994
|
-
expire=self._get_next_secret_expiry_time(certificate),
|
|
1995
|
-
)
|
|
1996
|
-
self.on.certificate_available.emit(
|
|
1997
|
-
certificate_signing_request=certificate.csr,
|
|
1998
|
-
certificate=certificate.certificate,
|
|
1999
|
-
ca=certificate.ca,
|
|
2000
|
-
chain=certificate.chain,
|
|
2001
|
-
)
|
|
2002
|
-
|
|
2003
|
-
def _get_next_secret_expiry_time(self, certificate: ProviderCertificate) -> Optional[datetime]:
|
|
2004
|
-
"""Return the expiry time or expiry notification time.
|
|
2005
|
-
|
|
2006
|
-
Extracts the expiry time from the provided certificate, calculates the
|
|
2007
|
-
expiry notification time and return the closest of the two, that is in
|
|
2008
|
-
the future.
|
|
2009
|
-
|
|
2010
|
-
Args:
|
|
2011
|
-
certificate: ProviderCertificate object
|
|
2012
|
-
|
|
2013
|
-
Returns:
|
|
2014
|
-
Optional[datetime]: None if the certificate expiry time cannot be read,
|
|
2015
|
-
next expiry time otherwise.
|
|
2016
|
-
"""
|
|
2017
|
-
if not certificate.expiry_time or not certificate.expiry_notification_time:
|
|
2018
|
-
return None
|
|
2019
|
-
return _get_closest_future_time(
|
|
2020
|
-
certificate.expiry_notification_time,
|
|
2021
|
-
certificate.expiry_time,
|
|
2022
|
-
)
|
|
2023
|
-
|
|
2024
|
-
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
2025
|
-
"""Handle Relation Broken Event.
|
|
2026
|
-
|
|
2027
|
-
Emitting `all_certificates_invalidated` from `relation-broken` rather
|
|
2028
|
-
than `relation-departed` since certs are stored in app data.
|
|
2029
|
-
|
|
2030
|
-
Args:
|
|
2031
|
-
event: Juju event
|
|
2032
|
-
|
|
2033
|
-
Returns:
|
|
2034
|
-
None
|
|
2035
|
-
"""
|
|
2036
|
-
self.on.all_certificates_invalidated.emit()
|
|
2037
|
-
|
|
2038
|
-
def _on_secret_expired(self, event: SecretExpiredEvent) -> None:
|
|
2039
|
-
"""Handle Secret Expired Event.
|
|
2040
|
-
|
|
2041
|
-
Loads the certificate from the secret, and will emit 1 of 2
|
|
2042
|
-
events.
|
|
2043
|
-
|
|
2044
|
-
If the certificate is not yet expired, emits CertificateExpiringEvent
|
|
2045
|
-
and updates the expiry time of the secret to the exact expiry time on
|
|
2046
|
-
the certificate.
|
|
2047
|
-
|
|
2048
|
-
If the certificate is expired, emits CertificateInvalidedEvent and
|
|
2049
|
-
deletes the secret.
|
|
2050
|
-
|
|
2051
|
-
Args:
|
|
2052
|
-
event (SecretExpiredEvent): Juju event
|
|
2053
|
-
"""
|
|
2054
|
-
csr = self._get_csr_from_secret(event.secret)
|
|
2055
|
-
if not csr:
|
|
2056
|
-
logger.error("Failed to get CSR from secret %s", event.secret.label)
|
|
2057
|
-
return
|
|
2058
|
-
provider_certificate = self._find_certificate_in_relation_data(csr)
|
|
2059
|
-
if not provider_certificate:
|
|
2060
|
-
# A secret expired but we did not find matching certificate. Cleaning up
|
|
2061
|
-
logger.warning(
|
|
2062
|
-
"Failed to find matching certificate for csr, cleaning up secret %s",
|
|
2063
|
-
event.secret.label,
|
|
2064
|
-
)
|
|
2065
|
-
event.secret.remove_all_revisions()
|
|
2066
|
-
return
|
|
2067
|
-
|
|
2068
|
-
if not provider_certificate.expiry_time:
|
|
2069
|
-
# A secret expired but matching certificate is invalid. Cleaning up
|
|
2070
|
-
logger.warning(
|
|
2071
|
-
"Certificate matching csr is invalid, cleaning up secret %s",
|
|
2072
|
-
event.secret.label,
|
|
2073
|
-
)
|
|
2074
|
-
event.secret.remove_all_revisions()
|
|
2075
|
-
return
|
|
2076
|
-
|
|
2077
|
-
if datetime.now(timezone.utc) < provider_certificate.expiry_time:
|
|
2078
|
-
logger.warning("Certificate almost expired")
|
|
2079
|
-
self.on.certificate_expiring.emit(
|
|
2080
|
-
certificate=provider_certificate.certificate,
|
|
2081
|
-
expiry=provider_certificate.expiry_time.isoformat(),
|
|
2082
|
-
)
|
|
2083
|
-
event.secret.set_info(
|
|
2084
|
-
expire=provider_certificate.expiry_time,
|
|
2085
|
-
)
|
|
2086
|
-
else:
|
|
2087
|
-
logger.warning("Certificate is expired")
|
|
2088
|
-
self.on.certificate_invalidated.emit(
|
|
2089
|
-
reason="expired",
|
|
2090
|
-
certificate=provider_certificate.certificate,
|
|
2091
|
-
certificate_signing_request=provider_certificate.csr,
|
|
2092
|
-
ca=provider_certificate.ca,
|
|
2093
|
-
chain=provider_certificate.chain,
|
|
2094
|
-
)
|
|
2095
|
-
self.request_certificate_revocation(provider_certificate.certificate.encode())
|
|
2096
|
-
event.secret.remove_all_revisions()
|
|
2097
|
-
|
|
2098
|
-
def _find_certificate_in_relation_data(self, csr: str) -> Optional[ProviderCertificate]:
|
|
2099
|
-
"""Return the certificate that match the given CSR."""
|
|
2100
|
-
for provider_certificate in self.get_provider_certificates():
|
|
2101
|
-
if provider_certificate.csr != csr:
|
|
2102
|
-
continue
|
|
2103
|
-
return provider_certificate
|
|
2104
|
-
return None
|
|
2105
|
-
|
|
2106
|
-
def _get_csr_from_secret(self, secret: Secret) -> Union[str, None]:
|
|
2107
|
-
"""Extract the CSR from the secret label or content.
|
|
2108
|
-
|
|
2109
|
-
This function is a workaround to maintain backwards compatibility
|
|
2110
|
-
and fix the issue reported in
|
|
2111
|
-
https://github.com/canonical/tls-certificates-interface/issues/228
|
|
2112
|
-
"""
|
|
2113
|
-
try:
|
|
2114
|
-
content = secret.get_content(refresh=True)
|
|
2115
|
-
except SecretNotFoundError:
|
|
2116
|
-
return None
|
|
2117
|
-
if not (csr := content.get("csr", None)):
|
|
2118
|
-
# In versions <14 of the Lib we were storing the CSR in the label of the secret
|
|
2119
|
-
# The CSR now is stored int the content of the secret, which was a breaking change
|
|
2120
|
-
# Here we get the CSR if the secret was created by an app using libpatch 14 or lower
|
|
2121
|
-
if secret.label and secret.label.startswith(f"{LIBID}-"):
|
|
2122
|
-
csr = secret.label[len(f"{LIBID}-") :]
|
|
2123
|
-
return csr
|