TLS Certificates Interface
- By Canonical Telco
Channel | Revision | Published | Runs on |
---|---|---|---|
latest/edge | 92 | Today |
juju deploy tls-certificates-interface --channel edge
Deploy Kubernetes operators easily with Juju, the Universal Operator Lifecycle Manager. Need a Kubernetes cluster? Install MicroK8s to create a full CNCF-certified Kubernetes system in under 60 seconds.
Platform:
charms.tls_certificates_interface.v3.tls_certificates
-
- Last updated Today
- Revision Library version 3.9
Library for the tls-certificates relation.
This library contains the Requires and Provides classes for handling the tls-certificates interface.
Pre-requisites:
- Juju >= 3.0
Getting Started
From a charm directory, fetch the library using charmcraft
:
charmcraft fetch-lib charms.tls_certificates_interface.v3.tls_certificates
Add the following libraries to the charm's requirements.txt
file:
- jsonschema
- cryptography >= 42.0.0
Add the following section to the charm's charmcraft.yaml
file:
parts:
charm:
build-packages:
- libffi-dev
- libssl-dev
- rustc
- cargo
Provider charm
The provider charm is the charm providing certificates to another charm that requires them. In
this example, the provider charm is storing its private key using a peer relation interface called
replicas
.
Example:
from charms.tls_certificates_interface.v3.tls_certificates import (
CertificateCreationRequestEvent,
CertificateRevocationRequestEvent,
TLSCertificatesProvidesV3,
generate_private_key,
)
from ops.charm import CharmBase, InstallEvent
from ops.main import main
from ops.model import ActiveStatus, WaitingStatus
def generate_ca(private_key: bytes, subject: str) -> str:
return "whatever ca content"
def generate_certificate(ca: str, private_key: str, csr: str) -> str:
return "Whatever certificate"
class ExampleProviderCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.certificates = TLSCertificatesProvidesV3(self, "certificates")
self.framework.observe(
self.certificates.on.certificate_request,
self._on_certificate_request
)
self.framework.observe(
self.certificates.on.certificate_revocation_request,
self._on_certificate_revocation_request
)
self.framework.observe(self.on.install, self._on_install)
def _on_install(self, event: InstallEvent) -> None:
private_key_password = b"banana"
private_key = generate_private_key(password=private_key_password)
ca_certificate = generate_ca(private_key=private_key, subject="whatever")
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
replicas_relation.data[self.app].update(
{
"private_key_password": "banana",
"private_key": private_key,
"ca_certificate": ca_certificate,
}
)
self.unit.status = ActiveStatus()
def _on_certificate_request(self, event: CertificateCreationRequestEvent) -> None:
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
ca_certificate = replicas_relation.data[self.app].get("ca_certificate")
private_key = replicas_relation.data[self.app].get("private_key")
certificate = generate_certificate(
ca=ca_certificate,
private_key=private_key,
csr=event.certificate_signing_request,
)
self.certificates.set_relation_certificate(
certificate=certificate,
certificate_signing_request=event.certificate_signing_request,
ca=ca_certificate,
chain=[ca_certificate, certificate],
relation_id=event.relation_id,
)
def _on_certificate_revocation_request(self, event: CertificateRevocationRequestEvent) -> None:
# Do what you want to do with this information
pass
if __name__ == "__main__":
main(ExampleProviderCharm)
Requirer charm
The requirer charm is the charm requiring certificates from another charm that provides them. In
this example, the requirer charm is storing its certificates using a peer relation interface called
replicas
.
Example:
from charms.tls_certificates_interface.v3.tls_certificates import (
CertificateAvailableEvent,
CertificateExpiringEvent,
CertificateRevokedEvent,
TLSCertificatesRequiresV3,
generate_csr,
generate_private_key,
)
from ops.charm import CharmBase, RelationJoinedEvent
from ops.main import main
from ops.model import ActiveStatus, WaitingStatus
from typing import Union
class ExampleRequirerCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.cert_subject = "whatever"
self.certificates = TLSCertificatesRequiresV3(self, "certificates")
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(
self.on.certificates_relation_joined, self._on_certificates_relation_joined
)
self.framework.observe(
self.certificates.on.certificate_available, self._on_certificate_available
)
self.framework.observe(
self.certificates.on.certificate_expiring, self._on_certificate_expiring
)
self.framework.observe(
self.certificates.on.certificate_invalidated, self._on_certificate_invalidated
)
self.framework.observe(
self.certificates.on.all_certificates_invalidated,
self._on_all_certificates_invalidated
)
def _on_install(self, event) -> None:
private_key_password = b"banana"
private_key = generate_private_key(password=private_key_password)
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
replicas_relation.data[self.app].update(
{"private_key_password": "banana", "private_key": private_key.decode()}
)
def _on_certificates_relation_joined(self, event: RelationJoinedEvent) -> None:
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
private_key_password = replicas_relation.data[self.app].get("private_key_password")
private_key = replicas_relation.data[self.app].get("private_key")
csr = generate_csr(
private_key=private_key.encode(),
private_key_password=private_key_password.encode(),
subject=self.cert_subject,
)
replicas_relation.data[self.app].update({"csr": csr.decode()})
self.certificates.request_certificate_creation(certificate_signing_request=csr)
def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
replicas_relation.data[self.app].update({"certificate": event.certificate})
replicas_relation.data[self.app].update({"ca": event.ca})
replicas_relation.data[self.app].update({"chain": event.chain})
self.unit.status = ActiveStatus()
def _on_certificate_expiring(
self, event: Union[CertificateExpiringEvent, CertificateInvalidatedEvent]
) -> None:
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
old_csr = replicas_relation.data[self.app].get("csr")
private_key_password = replicas_relation.data[self.app].get("private_key_password")
private_key = replicas_relation.data[self.app].get("private_key")
new_csr = generate_csr(
private_key=private_key.encode(),
private_key_password=private_key_password.encode(),
subject=self.cert_subject,
)
self.certificates.request_certificate_renewal(
old_certificate_signing_request=old_csr,
new_certificate_signing_request=new_csr,
)
replicas_relation.data[self.app].update({"csr": new_csr.decode()})
def _certificate_revoked(self) -> None:
old_csr = replicas_relation.data[self.app].get("csr")
private_key_password = replicas_relation.data[self.app].get("private_key_password")
private_key = replicas_relation.data[self.app].get("private_key")
new_csr = generate_csr(
private_key=private_key.encode(),
private_key_password=private_key_password.encode(),
subject=self.cert_subject,
)
self.certificates.request_certificate_renewal(
old_certificate_signing_request=old_csr,
new_certificate_signing_request=new_csr,
)
replicas_relation.data[self.app].update({"csr": new_csr.decode()})
replicas_relation.data[self.app].pop("certificate")
replicas_relation.data[self.app].pop("ca")
replicas_relation.data[self.app].pop("chain")
self.unit.status = WaitingStatus("Waiting for new certificate")
def _on_certificate_invalidated(self, event: CertificateInvalidatedEvent) -> None:
replicas_relation = self.model.get_relation("replicas")
if not replicas_relation:
self.unit.status = WaitingStatus("Waiting for peer relation to be created")
event.defer()
return
if event.reason == "revoked":
self._certificate_revoked()
if event.reason == "expired":
self._on_certificate_expiring(event)
def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEvent) -> None:
# Do what you want with this information, probably remove all certificates.
pass
if __name__ == "__main__":
main(ExampleRequirerCharm)
You can relate both charms by running:
juju relate <tls-certificates provider charm> <tls-certificates requirer charm>
Index
-
def generate_ca( private_key, subject, private_key_password, validity, country )
-
def get_certificate_extensions( authority_key_identifier, csr, alt_names, is_ca )
-
def generate_certificate( csr, ca, ca_key, ca_key_password, validity, alt_names, is_ca )
-
def generate_private_key( password, key_size, public_exponent )
class RequirerCSR
Description
This class represents a certificate signing request from an interface Requirer. None
class ProviderCertificate
Description
This class represents a certificate from an interface Provider. None
Methods
ProviderCertificate. chain_as_pem( self )
Description
Return full certificate chain as a PEM string. None
class CertificateAvailableEvent
Description
Charm Event triggered when a TLS certificate is available. None
Methods
CertificateAvailableEvent. __init__( self , handle: Handle , certificate: str , certificate_signing_request: str , ca: str , chain )
CertificateAvailableEvent. snapshot( self )
Description
Return snapshot. None
CertificateAvailableEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
CertificateAvailableEvent. chain_as_pem( self )
Description
Return full certificate chain as a PEM string. None
class CertificateExpiringEvent
Description
Charm Event triggered when a TLS certificate is almost expired. None
Methods
CertificateExpiringEvent. __init__( self , handle , certificate: str , expiry: str )
CertificateExpiringEvent.
Arguments
Juju framework handle
TLS Certificate
Datetime string representing the time at which the certificate won't be valid anymore.
CertificateExpiringEvent. snapshot( self )
Description
Return snapshot. None
CertificateExpiringEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
class CertificateInvalidatedEvent
Description
Charm Event triggered when a TLS certificate is invalidated. None
Methods
CertificateInvalidatedEvent. __init__( self , handle: Handle , reason , certificate: str , certificate_signing_request: str , ca: str , chain )
CertificateInvalidatedEvent. snapshot( self )
Description
Return snapshot. None
CertificateInvalidatedEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
class AllCertificatesInvalidatedEvent
Description
Charm Event triggered when all TLS certificates are invalidated. None
Methods
AllCertificatesInvalidatedEvent. __init__( self , handle: Handle )
AllCertificatesInvalidatedEvent. snapshot( self )
Description
Return snapshot. None
AllCertificatesInvalidatedEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
class CertificateCreationRequestEvent
Description
Charm Event triggered when a TLS certificate is required. None
Methods
CertificateCreationRequestEvent. __init__( self , handle: Handle , certificate_signing_request: str , relation_id: int , is_ca: bool )
CertificateCreationRequestEvent. snapshot( self )
Description
Return snapshot. None
CertificateCreationRequestEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
class CertificateRevocationRequestEvent
Description
Charm Event triggered when a TLS certificate needs to be revoked. None
Methods
CertificateRevocationRequestEvent. __init__( self , handle: Handle , certificate: str , certificate_signing_request: str , ca: str , chain: str )
CertificateRevocationRequestEvent. snapshot( self )
Description
Return snapshot. None
CertificateRevocationRequestEvent. restore( self , snapshot: dict )
Description
Restore snapshot. None
def
generate_ca(
private_key: bytes,
subject: str,
private_key_password,
validity: int,
country: str
)
Generate a CA Certificate.
Arguments
Private key
Common Name that can be an IP or a Full Qualified Domain Name (FQDN).
Private key password
Certificate validity time (in days)
Certificate Issuing country
Returns
CA Certificate.
def
get_certificate_extensions(
authority_key_identifier: bytes,
csr,
alt_names,
is_ca: bool
)
Generate a list of certificate extensions from a CSR and other known information.
Arguments
Authority key identifier
CSR
List of alt names to put on cert - prefer putting SANs in CSR
Whether the certificate is a CA certificate
Returns
List of extensions
def
generate_certificate(
csr: bytes,
ca: bytes,
ca_key: bytes,
ca_key_password,
validity: int,
alt_names,
is_ca: bool
)
Generate a TLS certificate based on a CSR.
Arguments
CSR
CA Certificate
CA private key
CA private key password
Certificate validity (in days)
List of alt names to put on cert - prefer putting SANs in CSR
Whether the certificate is a CA certificate
Returns
Certificate
def
generate_private_key(
password,
key_size: int,
public_exponent: int
)
Generate a private key.
Arguments
Password for decrypting the private key
Key size in bytes
Public exponent.
Returns
Private Key
def
generate_csr(
private_key: bytes,
subject: str,
add_unique_id_to_subject_name: bool,
organization,
email_address,
country_name,
private_key_password,
sans,
sans_oid,
sans_ip,
sans_dns,
additional_critical_extensions
)
Generate a CSR using private key and subject.
Arguments
Private key
CSR Common Name that can be an IP or a Full Qualified Domain Name (FQDN).
Whether a unique ID must be added to the CSR's subject name. Always leave to "True" when the CSR is used to request certificates using the tls-certificates relation.
Name of organization.
Email address.
Country Name.
Private key password
Use sans_dns - this will be deprecated in a future release List of DNS subject alternative names (keeping it for now for backward compatibility)
List of registered ID SANs
List of DNS subject alternative names (similar to the arg: sans)
List of IP subject alternative names
List of critical additional extension objects. Object must be a x509 ExtensionType.
Returns
CSR
def
csr_matches_certificate(
csr: str,
cert: str
)
Check if a CSR matches a certificate.
Arguments
Certificate Signing Request as a string
Certificate as a string
Returns
True/False depending on whether the CSR matches the certificate.
class CertificatesProviderCharmEvents
Description
List of events that the TLS Certificates provider charm can leverage. None
class CertificatesRequirerCharmEvents
Description
List of events that the TLS Certificates requirer charm can leverage. None
class TLSCertificatesProvidesV3
Description
TLS certificates provider class to be instantiated by TLS certificates providers. None
Methods
TLSCertificatesProvidesV3. __init__( self , charm: CharmBase , relationship_name: str )
TLSCertificatesProvidesV3. revoke_all_certificates( self )
Revoke all certificates of this provider.
Description
This method is meant to be used when the Root CA has changed.
TLSCertificatesProvidesV3. set_relation_certificate( self , certificate: str , certificate_signing_request: str , ca: str , chain , relation_id: int )
Add certificates to relation data.
Arguments
Certificate
Certificate signing request
CA Certificate
CA Chain
Juju relation ID
Returns
None
TLSCertificatesProvidesV3. remove_certificate( self , certificate: str )
Remove a given certificate from relation data.
Arguments
TLS Certificate
Returns
None
TLSCertificatesProvidesV3. get_issued_certificates( self , relation_id )
Return a List of issued (non revoked) certificates.
Returns
List of ProviderCertificate objects
TLSCertificatesProvidesV3. get_provider_certificates( self , relation_id )
Return a List of issued certificates.
Returns
List of ProviderCertificate objects
TLSCertificatesProvidesV3. get_outstanding_certificate_requests( self , relation_id )
Return CSR's for which no certificate has been issued.
Arguments
Relation id
Returns
List of RequirerCSR objects.
TLSCertificatesProvidesV3. get_requirer_csrs( self , relation_id )
Return a list of requirers' CSRs.
Returns
List[RequirerCSR]
Description
It returns CSRs from all relations if relation_id is not specified. CSRs are returned per relation id, application name and unit name.
TLSCertificatesProvidesV3. certificate_issued_for_csr( self , app_name: str , csr: str , relation_id )
Check whether a certificate has been issued for a given CSR.
Arguments
Application name that the CSR belongs to.
Certificate Signing Request.
Relation ID
Returns
True/False depending on whether a certificate has been issued for the given CSR.
class TLSCertificatesRequiresV3
Description
TLS certificates requirer class to be instantiated by TLS certificates requirers. None
Methods
TLSCertificatesRequiresV3. __init__( self , charm: CharmBase , relationship_name: str , expiry_notification_time: int )
Generate/use private key and observes relation changed event.
Arguments
Charm object
Juju relation name
Time difference between now and expiry (in hours). Used to trigger the CertificateExpiring event. Default: 7 days.
TLSCertificatesRequiresV3. get_requirer_csrs( self )
Return list of requirer's CSRs from relation unit data.
Returns
List of RequirerCSR objects.
TLSCertificatesRequiresV3. get_provider_certificates( self )
Description
Return list of certificates from the provider's relation data. None
TLSCertificatesRequiresV3. request_certificate_creation( self , certificate_signing_request: bytes , is_ca: bool )
Request TLS certificate to provider charm.
Arguments
Certificate Signing Request
Whether the certificate is a CA certificate
Returns
None
TLSCertificatesRequiresV3. request_certificate_revocation( self , certificate_signing_request: bytes )
Remove CSR from relation data.
Arguments
Certificate Signing Request
Returns
None
Description
The provider of this relation is then expected to remove certificates associated to this CSR from the relation data as well and emit a request_certificate_revocation event for the provider charm to interpret.
TLSCertificatesRequiresV3. request_certificate_renewal( self , old_certificate_signing_request: bytes , new_certificate_signing_request: bytes )
Renew certificate.
Arguments
Old CSR
New CSR
Returns
None
Description
Removes old CSR from relation data and adds new one.
TLSCertificatesRequiresV3. get_assigned_certificates( self )
Get a list of certificates that were assigned to this unit.
Returns
List[ProviderCertificate]
TLSCertificatesRequiresV3. get_expiring_certificates( self )
Get a list of certificates that were assigned to this unit that are expiring or expired.
Returns
List[ProviderCertificate]
TLSCertificatesRequiresV3. get_certificate_signing_requests( self , fulfilled_only: bool , unfulfilled_only: bool )
Get the list of CSR's that were sent to the provider.
Arguments
This option will discard CSRs that don't have certificates yet.
This option will discard CSRs that have certificates signed.
Returns
List of RequirerCSR objects.
Description
You can choose to get only the CSR's that have a certificate assigned or only the CSR's that don't.