-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #79 from charmed-kubernetes/feature/arch/multiarch
Remove python cryptography libraries in favor of subprocess to `openssl`
- Loading branch information
Showing
10 changed files
with
355 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# Copyright 2021 Canonical Ltd. | ||
# See LICENSE file for licensing details. | ||
|
||
"""# Self-Signed Certificate Generator. | ||
This charm library contains a class `SelfSignedCert` which can be used for generating self-signed | ||
RSA certificates for use in TLS connections or otherwise. It does not currently provide much | ||
configurability, apart from the FQDN the certificate should be associated with, a list of IP | ||
addresses to be present in the Subject Alternative Name (SAN) field, validity and key length. | ||
By default, generated certificates are valid for 365 years, and use a 2048-bit key size. | ||
## Getting Started | ||
In order to use this library, you will need to fetch the library from Charmhub as normal, but you | ||
will also need to add a dependency on the `cryptography` package to your charm: | ||
```shell | ||
cd some-charm | ||
charmcraft fetch-lib charms.kubernetes_dashboard.v1.cert | ||
echo <<-EOF >> requirements.txt | ||
cryptography | ||
EOF | ||
``` | ||
Once complete, you can import the charm and use it like so (in the most simple form): | ||
```python | ||
# ... | ||
from charms.kubernetes_dashboard.v0.cert import SelfSignedCert | ||
from ipaddress import IPv4Address | ||
# Generate a certificate | ||
self_signed_cert = SelfSigned(names=["test-service.dev"], ips=[IPv4Address("10.28.0.20")]) | ||
# Bytes representing the certificate in PEM format | ||
certificate = self_signed_cert.cert | ||
# Bytes representing the private key in PEM/PKCS8 format | ||
key = self_signed_cert.key | ||
``` | ||
You can also specify the validity period in days, and the required key size. The algorithm is | ||
always RSA: | ||
```python | ||
# ... | ||
from charms.kubernetes_dashboard.v0.cert import SelfSignedCert | ||
from ipaddress import IPv4Address | ||
# Generate a certificate | ||
self_signed_cert = SelfSigned( | ||
names=["some_app.my_namespace.svc.cluster.local"], | ||
ips=[IPv4Address("10.41.150.12"), IPv4Address("192.168.0.20")], | ||
key_size = 4096, | ||
validity = 3650 | ||
) | ||
``` | ||
""" | ||
|
||
# The unique Charmhub library identifier, never change it | ||
LIBID = "6b649bc0040448399cfc718a6fcba24d" | ||
|
||
# Increment this major API version when introducing breaking changes | ||
LIBAPI = 1 | ||
|
||
# Increment this PATCH version before using `charmcraft publish-lib` or reset | ||
# to 0 if you are raising the major API version | ||
LIBPATCH = 0 | ||
|
||
|
||
from datetime import datetime, timedelta | ||
from ipaddress import IPv4Address | ||
from typing import List | ||
from pathlib import Path | ||
from tempfile import NamedTemporaryFile | ||
from subprocess import check_call, check_output, CalledProcessError | ||
|
||
|
||
class SelfSignedCert: | ||
"""A class used for generating self-signed RSA TLS certificates.""" | ||
|
||
def __init__( | ||
self, | ||
*, | ||
names: List[str], | ||
ips: List[IPv4Address] = [], | ||
key_size: int = 2048, | ||
validity: int = 365, | ||
): | ||
"""Initialise a new self-signed certificate. | ||
Args: | ||
names: A list of FQDNs that should be placed in the Subject Alternative | ||
Name field of the certificate. The first name in the list will be | ||
used as the Common Name, Subject and Issuer field. | ||
ips: A list of IPv4Address objects that should be present in the list | ||
of Subject Alternative Names of the certificate. | ||
key_size: Size of the RSA Private Key to be generated. Defaults to 2048 | ||
validity: Period in days the certificate is valid for. Default is 365. | ||
Raises: | ||
ValueError: is raised if an empty list of names is provided to the | ||
constructor. | ||
""" | ||
|
||
# Ensure that at least one FQDN was provided | ||
# TODO: Do some validation on any provided names | ||
if not names: | ||
raise ValueError("Must provide at least one name for the certificate") | ||
|
||
# Create a list of x509.DNSName objects from the list of FQDNs provided | ||
self.names = names | ||
# Create a list of x509IPAdress objects from the list of IPv4Addresses | ||
self.ips = ips | ||
# Initialise some values | ||
self.key_size = key_size | ||
self.validity = validity | ||
self.cert = None | ||
self.key = None | ||
# Generate the certificate | ||
self._generate() | ||
|
||
def _generate(self) -> None: | ||
"""Generate a self-signed certificate.""" | ||
|
||
_binary = Path(__file__).parent / "gen-certificate.sh" | ||
_args: List[str] = [ | ||
"--names", | ||
",".join(self.names), | ||
"--ips", | ||
",".join(map(str, self.ips)), | ||
"--keysize", | ||
str(self.key_size), | ||
"--days", | ||
str(self.validity), | ||
] | ||
check_call([_binary, *_args]) | ||
self.ca = Path("/tmp/ca.crt").read_bytes() | ||
self.cert = Path("/tmp/server.crt").read_bytes() | ||
self.key = Path("/tmp/server.key").read_bytes() | ||
|
||
@staticmethod | ||
def validate_cert_date(in_crt: bytes) -> bool: | ||
with NamedTemporaryFile(mode="w+b") as crt: | ||
crt.write(in_crt) | ||
crt.flush() | ||
try: | ||
cmd = f"openssl x509 -in {crt.name} -noout -dates".split() | ||
dates = check_output(cmd, text=True) | ||
except CalledProcessError: | ||
return False | ||
before, after = [ | ||
datetime.strptime(_.split("=")[1], "%b %d %H:%M:%S %Y %Z") for _ in dates.splitlines() | ||
] | ||
now = datetime.utcnow() | ||
return before <= now <= after | ||
|
||
@staticmethod | ||
def sans_from_cert(in_crt: bytes) -> List[str]: | ||
with NamedTemporaryFile(mode="w+b") as crt: | ||
crt.write(in_crt) | ||
crt.flush() | ||
try: | ||
cmd = f"openssl x509 -in {crt.name} -noout -ext subjectAltName".split() | ||
output = check_output(cmd, text=True) | ||
except CalledProcessError: | ||
return [] | ||
return [ | ||
dns.split(":", 1)[1] | ||
for _ in output.splitlines() | ||
if "DNS:" in _ | ||
for dns in _.split(", ") | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
#!/bin/bash | ||
set -e | ||
|
||
usage() { | ||
cat <<EOF | ||
Generate certificate suitable for use with a service. | ||
This script uses k8s' CertificateSigningRequest API to generate a | ||
certificate signed by k8s CA suitable for use with webhook | ||
services. This requires permissions to create and approve CSR. See | ||
https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster for | ||
detailed explanation and additional instructions. | ||
usage: ${0} [OPTIONS] | ||
The following flags are required. | ||
--names Comma separated list of dns names to associate with cert | ||
--ips Comma separated list of ips to associate with cert Default: Empty Set | ||
--namespace Namespace where webhook service resides. Default: 'default' | ||
--keysize a bit length of at least 2048 when using RSA. Default: 2048 | ||
--days Period in days the certificate is valid for. Default: 3650 | ||
EOF | ||
exit 0 | ||
} | ||
|
||
while [[ $# -gt 0 ]]; do | ||
case ${1} in | ||
--namespace) | ||
NAMESPACE="$2" | ||
shift | ||
;; | ||
--keysize) | ||
KEYSIZE="$2" | ||
shift | ||
;; | ||
--days) | ||
DAYS="$2" | ||
shift | ||
;; | ||
--ips) | ||
IPS=(${2//,/ }) | ||
shift | ||
;; | ||
--names) | ||
NAMES=(${2//,/ }) | ||
shift | ||
;; | ||
*) | ||
usage | ||
;; | ||
esac | ||
shift | ||
done | ||
|
||
if [ ${#NAMES[@]} -eq 0 ]; then | ||
echo "'--names' must be specified" | ||
exit 1 | ||
fi | ||
|
||
[[ ${#IPS[@]} -eq 0 ]] && IPS=() | ||
[[ -z ${KEYSIZE} ]] && KEYSIZE=2048 | ||
[[ -z ${DAYS} ]] && DAYS=3650 | ||
|
||
if [[ ! -x "$(command -v openssl)" ]]; then | ||
echo "openssl not found" | ||
exit 1 | ||
fi | ||
|
||
CERTDIR=/tmp | ||
|
||
function createCerts() { | ||
echo "creating certs in dir ${CERTDIR} " | ||
|
||
cat <<EOF > ${CERTDIR}/csr.conf | ||
[req] | ||
default_bits = ${KEYSIZE} | ||
distinguished_name = req_distinguished_name | ||
req_extensions = req_ext | ||
x509_extentions = v3_req | ||
[req_distinguished_name] | ||
[req_ext] | ||
subjectAltName = @alt_names | ||
[ v3_req ] | ||
basicConstraints = CA:FALSE | ||
keyUsage = digitalSignature, keyEncipherment | ||
extendedKeyUsage = serverAuth, clientAuth | ||
subjectAltName = @alt_names | ||
[alt_names] | ||
EOF | ||
|
||
length=${#NAMES[@]} | ||
for (( i=0; i<${length}; i++)); do | ||
echo "DNS.$(($i+1)) = ${NAMES[$i]}" >> ${CERTDIR}/csr.conf | ||
done | ||
|
||
length=${#IPS[@]} | ||
for (( j=0; j<${length}; j++)); do | ||
echo "DNS.$(($j+$i+1)) = ${IPS[$j]}" >> ${CERTDIR}/csr.conf | ||
done | ||
|
||
openssl genrsa -out ${CERTDIR}/ca.key ${KEYSIZE} | ||
openssl req -x509 -new -nodes -key ${CERTDIR}/ca.key -subj "/CN=${NAMES[0]}" -days ${DAYS} -out ${CERTDIR}/ca.crt | ||
|
||
openssl genrsa -out ${CERTDIR}/server.key ${KEYSIZE} | ||
openssl req -new -key ${CERTDIR}/server.key -subj "/CN=${NAMES[0]}" -out ${CERTDIR}/server.csr -config ${CERTDIR}/csr.conf | ||
|
||
openssl x509 -req -in ${CERTDIR}/server.csr -CA ${CERTDIR}/ca.crt -CAkey ${CERTDIR}/ca.key \ | ||
-CAcreateserial -out ${CERTDIR}/server.crt \ | ||
-extensions v3_req -extfile ${CERTDIR}/csr.conf -days ${DAYS} | ||
} | ||
|
||
createCerts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
ops==2.2.0 | ||
cryptography==40.0.1 | ||
lightkube==0.12.0 | ||
lightkube-models==1.26.0.4 | ||
Jinja2==3.1.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.