Keeping Track of Certificate Expiry with a JWKS to iCalendar Converter

Why?

I work with a number of certificates, and probably the thing I like about them most (but also least!) is how they have a built-in requirement for regular rotation.

Because I work with a number of certificates, I want to make sure that I know ahead of time when I need to plan to rotate them, instead of leaving it until it's last minute (at least for cases where I'm not using Let's Encrypt).

The best way I've found this works is by tracking their expiries in my calendar, so as I'm looking week(s) into the future, I can see what needs to be rotated soon.

I've looked at writing one-off scripts to set these alerts up, but wasn't as much of a fan, as I'd need to keep re-running the scripts.

Because the certs I'm using are generally exposed on a JSON Web Key Set (JWKS) endpoint, I decided to build on the idea of these scripts to generate calendar entries, and have created a lightweight API that can take a given JWKS, parse the X509 certificate from the x5c field, and return an iCalendar feed.

Demo

You can interact with the API by performing a GET request on the below URL:

https://europe-west1-jwksical-jvt-me.cloudfunctions.net/jwks-ical
  ?jwks_uri=$jwks_uri

Where jwks_uri is a URI for a given JWKS endpoint, such as https://jamietanna.eu.auth0.com/.well-known/jwks.json:

{
  "keys": [
    {
      "alg": "RS256",
      "kty": "RSA",
      "use": "sig",
      "n": "0gDujz_AKJEPpwagtlMu6fjEC6Sjsy28G4vqh7nM13FdN39DuKn1NGFyYbvFtmKMzv1vnf-vRVbbuWhhQYmsApY8T4C8mf_JWOZmOpN_tkehdSzExfQt8nAJtpMYJWEoF61xJkrIqUiAi3diE6EKlpAJ1xTlbKv8SP3O3VLz8pvAQAoSqIm9A9BjVZ1QVGTJBdZwvNccDOFo9yHPDsN3j0cE0WlJal_BLE7w2zvUS3gCxkqzedMR5x_74tPhWKv0jRq38UWemnNGJ51PY4oZnRGJulLbtlWDekdRBhTubqQeFECf0pDtyIi2lTVvNiz0G1rCTEPnF5OgPto5HU16iw",
      "e": "AQAB",
      "kid": "ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzUxQzc5MUY0MjI2Nw",
      "x5t": "ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzUxQzc5MUY0MjI2Nw",
      "x5c": [
        "MIIDCTCCAfGgAwIBAgIJQGn5s3JXIPp1MA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNVBAMTF2phbWlldGFubmEuZXUuYXV0aDAuY29tMB4XDTE4MTAxMzEyMDEzMloXDTMyMDYyMTEyMDEzMlowIjEgMB4GA1UEAxMXamFtaWV0YW5uYS5ldS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSAO6PP8AokQ+nBqC2Uy7p+MQLpKOzLbwbi+qHuczXcV03f0O4qfU0YXJhu8W2YozO/W+d/69FVtu5aGFBiawCljxPgLyZ/8lY5mY6k3+2R6F1LMTF9C3ycAm2kxglYSgXrXEmSsipSICLd2IToQqWkAnXFOVsq/xI/c7dUvPym8BAChKoib0D0GNVnVBUZMkF1nC81xwM4Wj3Ic8Ow3ePRwTRaUlqX8EsTvDbO9RLeALGSrN50xHnH/vi0+FYq/SNGrfxRZ6ac0YnnU9jihmdEYm6Utu2VYN6R1EGFO5upB4UQJ/SkO3IiLaVNW82LPQbWsJMQ+cXk6A+2jkdTXqLAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFA/S9SidqazcF0a8muCcUT6HWFWPMA4GA1UdDwEB/wQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEAJSiHw0030rEWRB8sF/RgiHjDa8X6Yr7xP5KrbzmRIvnjjO0DpeOW8DOJ4YD++Hv0aUjvJ9xzXxjeeFiXMyLlpnknws1GXANYvx/o7ss1TWTVKRqKdviq8OoDPPvloL8EIUyFuTHw4e/Lapejd8hOs91pEXXVPEeHF4QQSH2cqqZ1fpmzgLtfFsCkQyCcwEY7imp49VYiaTuGtiIFn5gzxu/fS3RGqzwOIMZAEs82d2jINHP29lfalNCW1lYI9PUN/fIBtLV84x15iOJNMW5M2Th/9y6eirry/TCY9OMc+Xp8Eq44wZinWrezK7ges1Xa1XDY8AMylJ4Ai2WH6JgCuw=="
      ]
    },
    {
      "alg": "RS256",
      "kty": "RSA",
      "use": "sig",
      "n": "nPp-Jdf4tOYClmGLn7XewSoRNqEI59kAmAezojE0kX3OueeekJTN4A-kLN9StrO0MNfORkeGbAdYle3fIXN3TyFn-9MwTysCX4GQmTpRE2kEktHy1Gjd_7sGq7vw7_UfP2zwBMlsLt4JLElAhi_dwbASPvKPA5f1Sf-I67x4fN2iGdoRyGJauJ3vRiKhd_qXL7rh2Yfx3SCohEtNji_BwNtiRDSUgD2LOMN8NZ-3TlLRvZk5tN44Oe0_x9QTkCFqq6FngtC1HGHcPOj05DPM2H2_1GC5g156gJroUKMuahY4vdOKJaTecQ29e9Uz6aQrtl4GueeWwQ560v4t70HCMw",
      "e": "AQAB",
      "kid": "wWbdWIDy-Op5zSH4u53PM",
      "x5t": "351BhlP9swkmXZXjisbkeJkUSp8",
      "x5c": [
        "MIIDCTCCAfGgAwIBAgIJQH5gIZjaIVaqMA0GCSqGSIb3DQEBCwUAMCIxIDAeBgNVBAMTF2phbWlldGFubmEuZXUuYXV0aDAuY29tMB4XDTIwMDMxMzExNTg1N1oXDTMzMTEyMDExNTg1N1owIjEgMB4GA1UEAxMXamFtaWV0YW5uYS5ldS5hdXRoMC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc+n4l1/i05gKWYYuftd7BKhE2oQjn2QCYB7OiMTSRfc65556QlM3gD6Qs31K2s7Qw185GR4ZsB1iV7d8hc3dPIWf70zBPKwJfgZCZOlETaQSS0fLUaN3/uwaru/Dv9R8/bPAEyWwu3gksSUCGL93BsBI+8o8Dl/VJ/4jrvHh83aIZ2hHIYlq4ne9GIqF3+pcvuuHZh/HdIKiES02OL8HA22JENJSAPYs4w3w1n7dOUtG9mTm03jg57T/H1BOQIWqroWeC0LUcYdw86PTkM8zYfb/UYLmDXnqAmuhQoy5qFji904olpN5xDb171TPppCu2Xga555bBDnrS/i3vQcIzAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFDFn7euS5mK9s5Hzieby3Y6R4Z/RMA4GA1UdDwEB/wQEAwIChDANBgkqhkiG9w0BAQsFAAOCAQEATqkteUMQsqjjbsStWXsQj3zz9aMA39q8exaogcV0T9JexN2hgunJGJAVB5yDupikj4jP61Ji02hEhnql3q7Kig9+SYRkK1E5RI4WoHm0c2oXj6GoKYnwlGsEsW/bPUr5CSEQXLte6czlfSCr0VW5XV8YuVjiO9E7yARXcVywFdToipT8F0lMbugkVBJO2k/n4RpJ2nO0rH0TMdgQDXsotUdvLAbsvppJl/+LHknY0pN7dXMhZVCiyfdLE9/LdQJxlpmFi65FF2XMwr2x/uTcP4hytRaQIsU9mE3scPAx5RFxCppZu9BcVbAZTJFl/PemhonaZolrOMAo8hq0AbJ3rg=="
      ]
    }
  ]
}

(Aside: if you're interested in what the certs look like, check out Extracting x5cs from a JSON Web Key Set (JWKS) to PEM files with Ruby).

By running this JWKS URI through the jwks-ical API, you will be returned with the following iCalendar representation:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Michael Angstadt//biweekly 0.6.3//EN
BEGIN:VEVENT
UID:76becdc5-a56f-4c6b-ab92-c69ef2f52640
DTSTAMP:20200614T191639Z
DTSTART:20320621T080000Z
DTEND:20320621T200000Z
DESCRIPTION:The certificate for kid ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzU
 xQzc5MUY0MjI2Nw (with use: `sig` and subject: `CN=jamietanna.eu.auth0.com`
 ) is expiring on Mon Jun 21 12:01:32 UTC 2032
SUMMARY: Certificate expiry for ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzUxQzc
 5MUY0MjI2Nw
END:VEVENT
BEGIN:VEVENT
UID:5074509f-9fbe-4021-bd20-810d1e9d7c0a
DTSTAMP:20200614T191639Z
DTSTART:20331120T080000Z
DTEND:20331120T200000Z
DESCRIPTION:The certificate for kid wWbdWIDy-Op5zSH4u53PM (with use: `sig`
 and subject: `CN=jamietanna.eu.auth0.com`) is expiring on Sun Nov 20 11:58
 :57 UTC 2033
SUMMARY: Certificate expiry for wWbdWIDy-Op5zSH4u53PM
END:VEVENT
END:VCALENDAR

Which you can feed into your calendar reader of choice, and keep an eye on when your certificates are expiring.

Providing more event context

If you've got lots of certs you're keeping an eye of, or if you want to make it more obvious when you've got a production certificate expiring, you can provide a value for the querystring parameter prefix:

https://europe-west1-jwksical-jvt-me.cloudfunctions.net/jwks-ical
  ?jwks_uri=$jwks_uri
  &prefix=Auth0+Production

Which generates a slightly different event description, to make it a little more obvious:

 DESCRIPTION:The certificate for kid ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzU
  xQzc5MUY0MjI2Nw (with use: `sig` and subject: `CN=jamietanna.eu.auth0.com`
  ) is expiring on Mon Jun 21 12:01:32 UTC 2032
-SUMMARY: Certificate expiry for ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzUxQzc
+SUMMARY: Auth0 Production Certificate expiry for ODg2MzE1Qzg5Q0Y3ODdCNjcxNUU1RURERkMzMzUxQzc
  5MUY0MjI2Nw
 END:VEVENT

Insecure SSL/TLS configuration

Because I want to use this with Open Banking's certificates, I needed to disable the SSL/TLS validation, as https://keystore.openbanking.org.uk/ uses a self-signed certificate.

Is this a dealbreaker for your own use cases? Let me know, and I can move to a per-JWKS configuration for insecure connections.

Performance

This is a Java function, so note that performance isn't exactly going to be perfect. But a) I wanted to write it in Java and b) the performance doesn't need to be perfect, as it doesn't need to be realtime.

Vanity URL

This is my first experience with Google Cloud Functions, and I've found it pretty fun to work with. Unfortunately I couldn't get a custom domain working properly with Google Cloud Functions, even using Firebase, so for now you can use https://europe-west1-jwksical-jvt-me.cloudfunctions.net/jwks-ical.

If that's a bit of a mouthful, you can use https://u.jvt.me/jwks-ical to remember it.

Source Code

The source code for this can be found at jamietanna/jwks-ical, and is licensed under the AGPL3.

Feedback

Feedback is always welcome - please feel free to raise an issue on jamietanna/jwks-ical, or contact me with one of the methods below.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#java #jwks #certificates #jwks-ical #calendar #google-cloud #serverless.

Also on: Lobste.rs logo

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.