Skip to content

Commit

Permalink
feat: return TLS cert and key from get-certificate
Browse files Browse the repository at this point in the history
Change the get-certificate output to return a list of certificates for
the given FQDN. The list is sorted by decreasing relevance, so the first
item has the higher relevance. This is a list of relevance factors, from
most important to less important:

- custom certificate with wildcard match
- custom certificate with exact name match
- internal (ACME) certificate with wildcard match
- default internal certificate with exact name match
- internal certificate with exact name match

The match occurs in both the certificate subject and SAN extension.

If two certificates score the same relevance the order depends on the
underlying storage (custom certs or acme.json).

If no match is found, the self-signed certificate is returned.

Expiration date is not considered.
  • Loading branch information
DavidePrincipi committed Mar 6, 2025
1 parent f4cf443 commit 18f3dcb
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 49 deletions.
79 changes: 66 additions & 13 deletions imageroot/actions/get-certificate/20readconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,78 @@ import json
import sys
import os
import cert_helpers
import glob
import base64
import itertools

def main():
request = json.load(sys.stdin)
fqdn = request['fqdn']
if fqdn in cert_helpers.read_custom_cert_names():
response = {
"fqdn": fqdn,
"type": "custom",
"obtained": True,
}
elif fqdn in cert_helpers.read_default_cert_names():
response = {
"fqdn": fqdn,
"type": "internal",
"obtained": cert_helpers.has_acmejson_name(fqdn),
}
default_cert_names = set(cert_helpers.read_default_cert_names())
try:
wildcard = '*.' + fqdn.split(".", 1)[1]
except IndexError:
wildcard = None
certmatches = [] # list of certificates that match fqdn
for ctype, score, main, bcert, bkey, names in itertools.chain(
custom_certs_iterator(), # base score 20
acme_certs_iterator(), # base score 20
):
scert = base64.b64encode(bcert).decode('utf-8')
skey = base64.b64encode(bkey).decode('utf-8')
if wildcard and wildcard in names:
# +5 points if fqdn matches a wildcard name
certmatches.append((scert, skey, ctype, main, score + 5))
elif fqdn in names and ctype == 'internal' and default_cert_names == names:
# +2 points if fqdn matches the default acme.json certificate
certmatches.append((scert, skey, ctype, main, score + 2))
elif fqdn in names:
# base score if fqdn matches main or sans
certmatches.append((scert, skey, ctype, main, score))

if certmatches:
# Sort matches by score and convert in output format
obtained = True
certmatches.sort(key=lambda c: c[4], reverse=True) # use score to sort items
ctype = certmatches[0][2]
certificates = [{"cert": c[0], "key": c[1]} for c in certmatches]
else:
response = {}
# No match found, fall back to selfsigned certificate
obtained = False
ctype = "selfsigned"
scert = base64.b64encode(open("selfsigned.crt", "rb",).read()).decode('utf-8')
skey = base64.b64encode(open("selfsigned.key", "rb").read()).decode('utf-8')
certificates = [{"cert": scert, "key": skey}]

response = {
"fqdn": fqdn,
"type": ctype,
"obtained": obtained,
"certificates": certificates,
}
json.dump(response, fp=sys.stdout)

def custom_certs_iterator():
for cert_path in glob.glob("custom_certificates/*.crt"):
main = cert_path.removeprefix("custom_certificates/").removesuffix(".crt")
with open(cert_path, 'rb') as f:
bcert = f.read()
with open(f"custom_certificates/{main}.key", "rb") as f:
bkey = f.read()
names = cert_helpers.extract_certified_names(bcert)
yield 'custom', 20, main, bcert, bkey, names

def acme_certs_iterator():
try:
with open('acme/acme.json', 'r') as fp:
acmejson = json.load(fp)
for ocert in acmejson['acmeServer']["Certificates"] or []:
bcert = base64.b64decode(ocert["certificate"])
bkey = base64.b64decode(ocert["key"])
names = cert_helpers.extract_certified_names(bcert)
yield 'internal', 10, ocert["domain"]["main"], bcert, bkey, names
except (FileNotFoundError, KeyError, json.JSONDecodeError):
pass

if __name__ == "__main__":
main()
6 changes: 3 additions & 3 deletions imageroot/actions/get-certificate/validate-input.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
"description": "Get status of a requested certificate",
"examples": [
{
"fqdn": "example.com"
}
"fqdn": "example.com"
}
],
"type": "object",
"required": [
"fqdn"
],
"properties": {
"fqdn": {
"type":"string",
"type": "string",
"format": "hostname",
"title": "A fully qualified domain name"
}
Expand Down
78 changes: 45 additions & 33 deletions imageroot/actions/get-certificate/validate-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,59 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "get-certificate output",
"$id": "http://schema.nethserver.org/traefik/get-certificate-output.json",
"description": "Status of a requested certificate",
"description": "Get one or more certificates for the given FQDN.",
"examples": [
{
"fqdn": "example.com",
"obtained": true,
"type": "internal"
"type": "internal",
"certificates": [
{
"cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0F...",
"key": "LS0tLS1CRUdJTiBQUklWQVRFIEt..."
}
]
}
],
"type": "object",
"oneOf": [
{
"type": "object",
"properties": {
"fqdn": {
"type": "string",
"format": "hostname",
"title": "A fully qualified domain name"
},
"type": {
"type": "string",
"enum": [
"internal",
"custom",
"route"
],
"title": "must be route, internal or custom"
},
"obtained": {
"type": "boolean",
"title": "true if the certificate was obtained correctly"
}
},
"required": [
"fqdn",
"type",
"obtained"
"properties": {
"fqdn": {
"type": "string",
"format": "hostname",
"title": "A fully qualified domain name"
},
"type": {
"type": "string",
"enum": [
"internal",
"custom",
"selfsigned"
]
},
{
"type": "object",
"additionalProperties": false
"certificates": {
"type": "array",
"description": "List of certificates for FQDN, ordered by relevance (high first).",
"items": {
"type": "object",
"properties": {
"cert": {
"type": "string"
},
"key": {
"type": "string"
}
}
}
},
"obtained": {
"type": "boolean",
"title": "true if the certificate was obtained correctly"
}
},
"required": [
"fqdn",
"type",
"certificates",
"obtained"
]
}
}

0 comments on commit 18f3dcb

Please sign in to comment.