mongo-charms-single-kernel 1.8.8__py3-none-any.whl → 1.8.10__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.

Files changed (31) hide show
  1. {mongo_charms_single_kernel-1.8.8.dist-info → mongo_charms_single_kernel-1.8.10.dist-info}/METADATA +1 -1
  2. {mongo_charms_single_kernel-1.8.8.dist-info → mongo_charms_single_kernel-1.8.10.dist-info}/RECORD +30 -30
  3. single_kernel_mongo/config/literals.py +12 -5
  4. single_kernel_mongo/config/relations.py +2 -1
  5. single_kernel_mongo/config/statuses.py +127 -20
  6. single_kernel_mongo/core/operator.py +7 -0
  7. single_kernel_mongo/core/structured_config.py +2 -0
  8. single_kernel_mongo/core/workload.py +10 -4
  9. single_kernel_mongo/events/cluster.py +5 -0
  10. single_kernel_mongo/events/sharding.py +3 -1
  11. single_kernel_mongo/events/tls.py +183 -157
  12. single_kernel_mongo/exceptions.py +0 -8
  13. single_kernel_mongo/lib/charms/tls_certificates_interface/v4/tls_certificates.py +1995 -0
  14. single_kernel_mongo/managers/cluster.py +70 -28
  15. single_kernel_mongo/managers/config.py +24 -14
  16. single_kernel_mongo/managers/mongo.py +12 -12
  17. single_kernel_mongo/managers/mongodb_operator.py +58 -34
  18. single_kernel_mongo/managers/mongos_operator.py +16 -20
  19. single_kernel_mongo/managers/sharding.py +172 -136
  20. single_kernel_mongo/managers/tls.py +223 -206
  21. single_kernel_mongo/managers/upgrade_v3.py +6 -6
  22. single_kernel_mongo/state/charm_state.py +54 -31
  23. single_kernel_mongo/state/cluster_state.py +8 -0
  24. single_kernel_mongo/state/config_server_state.py +15 -6
  25. single_kernel_mongo/state/models.py +2 -2
  26. single_kernel_mongo/state/tls_state.py +39 -12
  27. single_kernel_mongo/utils/helpers.py +4 -19
  28. single_kernel_mongo/utils/mongodb_users.py +20 -20
  29. single_kernel_mongo/lib/charms/tls_certificates_interface/v3/tls_certificates.py +0 -2123
  30. {mongo_charms_single_kernel-1.8.8.dist-info → mongo_charms_single_kernel-1.8.10.dist-info}/WHEEL +0 -0
  31. {mongo_charms_single_kernel-1.8.8.dist-info → mongo_charms_single_kernel-1.8.10.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