2021-12-05 14:21:05 +00:00
package certificates
2021-03-16 23:34:31 +00:00
import (
2021-12-05 17:26:54 +00:00
"context"
2021-03-16 23:34:31 +00:00
"crypto/tls"
2021-07-08 23:15:42 +00:00
"crypto/x509"
2021-07-13 13:45:28 +00:00
"errors"
2021-12-05 22:20:34 +00:00
"fmt"
2021-12-01 21:49:48 +00:00
"strconv"
2021-07-08 23:15:42 +00:00
"strings"
"time"
2021-07-13 13:45:28 +00:00
"github.com/go-acme/lego/v4/certcrypto"
2021-12-03 02:44:21 +00:00
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
2021-07-13 13:45:28 +00:00
"github.com/go-acme/lego/v4/lego"
2024-05-26 20:05:46 +00:00
"github.com/hashicorp/golang-lru/v2/expirable"
2021-12-05 15:33:56 +00:00
"github.com/reugn/equalizer"
"github.com/rs/zerolog/log"
2021-12-03 02:44:21 +00:00
2021-12-03 03:15:48 +00:00
"codeberg.org/codeberg/pages/server/cache"
2021-12-03 02:44:21 +00:00
"codeberg.org/codeberg/pages/server/database"
2021-12-05 14:21:05 +00:00
dnsutils "codeberg.org/codeberg/pages/server/dns"
2022-06-11 21:02:06 +00:00
"codeberg.org/codeberg/pages/server/gitea"
2021-12-05 14:21:05 +00:00
"codeberg.org/codeberg/pages/server/upstream"
2021-03-16 23:34:31 +00:00
)
2023-02-11 02:29:08 +00:00
var ErrUserRateLimitExceeded = errors . New ( "rate limit exceeded: 10 certificates per user per 24 hours" )
2021-12-03 03:15:48 +00:00
// TLSConfig returns the configuration for generating, serving and cleaning up Let's Encrypt certificates.
2022-11-12 19:37:20 +00:00
func TLSConfig ( mainDomainSuffix string ,
2022-06-11 21:02:06 +00:00
giteaClient * gitea . Client ,
2023-02-11 02:29:08 +00:00
acmeClient * AcmeClient ,
2023-02-14 03:03:00 +00:00
firstDefaultBranch string ,
2024-05-26 20:05:46 +00:00
challengeCache , canonicalDomainCache cache . ICache ,
2022-03-27 19:54:06 +00:00
certDB database . CertDB ,
2024-04-18 17:05:20 +00:00
noDNS01 bool ,
rawDomain string ,
2022-03-27 19:54:06 +00:00
) * tls . Config {
2024-05-26 20:05:46 +00:00
// every cert is at most 24h in the cache and 7 days before expiry the cert is renewed
keyCache := expirable . NewLRU [ string , * tls . Certificate ] ( 32 , nil , 24 * time . Hour )
2021-12-05 13:45:17 +00:00
return & tls . Config {
// check DNS name & get certificate from Let's Encrypt
GetCertificate : func ( info * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
2023-02-13 20:14:45 +00:00
domain := strings . ToLower ( strings . TrimSpace ( info . ServerName ) )
if len ( domain ) < 1 {
return nil , errors . New ( "missing domain info via SNI (RFC 4366, Section 3.1)" )
2021-12-05 13:45:17 +00:00
}
2021-07-13 13:45:28 +00:00
2023-02-13 20:14:45 +00:00
// https request init is actually a acme challenge
2021-12-05 13:45:17 +00:00
if info . SupportedProtos != nil {
for _ , proto := range info . SupportedProtos {
2022-11-15 15:15:11 +00:00
if proto != tlsalpn01 . ACMETLS1Protocol {
continue
2021-07-13 13:45:28 +00:00
}
2023-02-14 02:23:28 +00:00
log . Info ( ) . Msgf ( "Detect ACME-TLS1 challenge for '%s'" , domain )
2022-11-15 15:15:11 +00:00
2023-02-13 20:14:45 +00:00
challenge , ok := challengeCache . Get ( domain )
2022-11-15 15:15:11 +00:00
if ! ok {
return nil , errors . New ( "no challenge for this domain" )
}
2023-02-13 20:14:45 +00:00
cert , err := tlsalpn01 . ChallengeCert ( domain , challenge . ( string ) )
2022-11-15 15:15:11 +00:00
if err != nil {
return nil , err
}
return cert , nil
2021-07-13 13:45:28 +00:00
}
}
2021-12-05 13:45:17 +00:00
targetOwner := ""
2023-02-10 01:38:15 +00:00
mayObtainCert := true
2024-04-18 17:05:20 +00:00
2023-02-13 20:14:45 +00:00
if strings . HasSuffix ( domain , mainDomainSuffix ) || strings . EqualFold ( domain , mainDomainSuffix [ 1 : ] ) {
2024-04-18 17:05:20 +00:00
if noDNS01 {
// Limit the domains allowed to request a certificate to pages-server domains
// and domains for an existing user of org
if ! strings . EqualFold ( domain , mainDomainSuffix [ 1 : ] ) && ! strings . EqualFold ( domain , rawDomain ) {
targetOwner := strings . TrimSuffix ( domain , mainDomainSuffix )
owner_exist , err := giteaClient . GiteaCheckIfOwnerExists ( targetOwner )
mayObtainCert = owner_exist
if err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "Failed to check '%s' existence on the forge: %s" , targetOwner , err )
mayObtainCert = false
}
}
} else {
// deliver default certificate for the main domain (*.codeberg.page)
domain = mainDomainSuffix
}
2021-11-20 14:30:58 +00:00
} else {
2021-12-05 13:45:17 +00:00
var targetRepo , targetBranch string
2024-05-26 20:05:46 +00:00
targetOwner , targetRepo , targetBranch = dnsutils . GetTargetFromDNS ( domain , mainDomainSuffix , firstDefaultBranch )
2021-12-05 13:45:17 +00:00
if targetOwner == "" {
// DNS not set up, return main certificate to redirect to the docs
2023-02-13 20:14:45 +00:00
domain = mainDomainSuffix
2021-12-05 13:45:17 +00:00
} else {
2022-11-12 19:43:44 +00:00
targetOpt := & upstream . Options {
TargetOwner : targetOwner ,
TargetRepo : targetRepo ,
TargetBranch : targetBranch ,
}
2023-02-13 20:14:45 +00:00
_ , valid := targetOpt . CheckCanonicalDomain ( giteaClient , domain , mainDomainSuffix , canonicalDomainCache )
2021-12-05 13:45:17 +00:00
if ! valid {
2023-02-10 01:38:15 +00:00
// We shouldn't obtain a certificate when we cannot check if the
// repository has specified this domain in the `.domains` file.
mayObtainCert = false
2021-12-05 13:45:17 +00:00
}
2021-11-20 14:30:58 +00:00
}
2021-07-13 13:45:28 +00:00
}
2023-02-13 20:14:45 +00:00
if tlsCertificate , ok := keyCache . Get ( domain ) ; ok {
2021-12-05 13:45:17 +00:00
// we can use an existing certificate object
2024-05-26 20:05:46 +00:00
return tlsCertificate , nil
2021-12-05 13:45:17 +00:00
}
2023-02-10 03:00:14 +00:00
var tlsCertificate * tls . Certificate
2021-12-05 13:45:17 +00:00
var err error
2023-02-13 20:14:45 +00:00
if tlsCertificate , err = acmeClient . retrieveCertFromDB ( domain , mainDomainSuffix , false , certDB ) ; err != nil {
if ! errors . Is ( err , database . ErrNotFound ) {
return nil , err
2021-12-05 13:45:17 +00:00
}
2023-02-13 20:14:45 +00:00
// we could not find a cert in db, request a new certificate
2021-11-20 14:30:58 +00:00
2023-02-13 20:14:45 +00:00
// first check if we are allowed to obtain a cert for this domain
if strings . EqualFold ( domain , mainDomainSuffix ) {
return nil , errors . New ( "won't request certificate for main domain, something really bad has happened" )
}
2023-02-10 01:38:15 +00:00
if ! mayObtainCert {
2023-02-13 20:14:45 +00:00
return nil , fmt . Errorf ( "won't request certificate for %q" , domain )
2023-02-10 01:38:15 +00:00
}
2023-02-13 20:14:45 +00:00
tlsCertificate , err = acmeClient . obtainCert ( acmeClient . legoClient , [ ] string { domain } , nil , targetOwner , false , mainDomainSuffix , certDB )
2021-12-05 13:45:17 +00:00
if err != nil {
return nil , err
}
2021-07-13 13:45:28 +00:00
}
2021-11-20 14:30:58 +00:00
2024-05-26 20:05:46 +00:00
keyCache . Add ( domain , tlsCertificate )
2023-02-10 03:00:14 +00:00
return tlsCertificate , nil
2021-12-05 13:45:17 +00:00
} ,
NextProtos : [ ] string {
2022-11-12 21:25:20 +00:00
"h2" ,
2021-12-05 13:45:17 +00:00
"http/1.1" ,
tlsalpn01 . ACMETLS1Protocol ,
} ,
2021-07-13 13:45:28 +00:00
2021-12-05 13:45:17 +00:00
// generated 2021-07-13, Mozilla Guideline v5.6, Go 1.14.4, intermediate configuration
// https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.6
MinVersion : tls . VersionTLS12 ,
CipherSuites : [ ] uint16 {
tls . TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,
tls . TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,
tls . TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 ,
tls . TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 ,
} ,
}
2021-07-13 08:28:36 +00:00
}
2023-02-11 02:29:08 +00:00
func ( c * AcmeClient ) checkUserLimit ( user string ) error {
userLimit , ok := c . acmeClientCertificateLimitPerUser [ user ]
2021-11-20 14:30:58 +00:00
if ! ok {
2023-02-11 02:29:08 +00:00
// Each user can only add 10 new domains per day.
2021-11-25 15:12:28 +00:00
userLimit = equalizer . NewTokenBucket ( 10 , time . Hour * 24 )
2023-02-11 02:29:08 +00:00
c . acmeClientCertificateLimitPerUser [ user ] = userLimit
2021-07-13 08:28:36 +00:00
}
2021-11-20 14:30:58 +00:00
if ! userLimit . Ask ( ) {
2023-02-11 02:29:08 +00:00
return fmt . Errorf ( "user '%s' error: %w" , user , ErrUserRateLimitExceeded )
2021-07-13 08:28:36 +00:00
}
2021-11-20 14:30:58 +00:00
return nil
2021-07-13 08:28:36 +00:00
}
2021-07-13 13:45:28 +00:00
2023-02-11 02:29:08 +00:00
func ( c * AcmeClient ) retrieveCertFromDB ( sni , mainDomainSuffix string , useDnsProvider bool , certDB database . CertDB ) ( * tls . Certificate , error ) {
2021-11-20 14:54:52 +00:00
// parse certificate from database
2022-11-15 15:15:11 +00:00
res , err := certDB . Get ( sni )
2021-12-05 18:00:57 +00:00
if err != nil {
2023-02-10 03:00:14 +00:00
return nil , err
} else if res == nil {
return nil , database . ErrNotFound
2021-11-20 14:54:52 +00:00
}
2021-11-20 18:36:12 +00:00
tlsCertificate , err := tls . X509KeyPair ( res . Certificate , res . PrivateKey )
2021-11-20 14:54:52 +00:00
if err != nil {
2023-02-10 03:00:14 +00:00
return nil , err
2021-11-20 14:54:52 +00:00
}
2021-11-20 18:36:12 +00:00
2021-12-02 18:12:45 +00:00
// TODO: document & put into own function
2022-11-12 19:37:20 +00:00
if ! strings . EqualFold ( sni , mainDomainSuffix ) {
2024-05-26 20:05:46 +00:00
tlsCertificate . Leaf , err = leaf ( & tlsCertificate )
2021-11-20 18:36:12 +00:00
if err != nil {
2024-05-26 20:05:46 +00:00
return nil , err
2021-11-20 18:36:12 +00:00
}
// renew certificates 7 days before they expire
2022-11-15 15:15:11 +00:00
if tlsCertificate . Leaf . NotAfter . Before ( time . Now ( ) . Add ( 7 * 24 * time . Hour ) ) {
2023-02-10 03:00:14 +00:00
// TODO: use ValidTill of custom cert struct
Implement static serving of compressed files (#387)
This provides an option for #223 without fully resolving it. (I think.)
Essentially, it acts very similar to the `gzip_static` and similar options for nginx, where it will check for the existence of pre-compressed files and serve those instead if the client allows it. I couldn't find a pre-existing way to actually parse the Accept-Encoding header properly (admittedly didn't look very hard) and just implemented one on my own that should be fine.
This should hopefully not have the same DOS vulnerabilities as #302, since it relies on the existing caching system. Compressed versions of files will be cached just like any other files, and that includes cache for missing files as well.
The compressed files will also be accessible directly, and this won't automatically decompress them. So, if you have a `tar.gz` file that you access directly, it will still be downloaded as the gzipped version, although you will now gain the option to download the `.tar` directly and decompress it in transit. (Which doesn't affect the server at all, just the client's way of interpreting it.)
----
One key thing this change also adds is a short-circuit when accessing directories: these always return 404 via the API, although they'd try the cache anyway and go through that route, which was kind of slow. Adding in the additional encodings, it's going to try for .gz, .br, and .zst files in the worst case as well, which feels wrong. So, instead, it just always falls back to the index-check behaviour if the path ends in a slash or is empty. (Which is implicitly just a slash.)
----
For testing, I set up this repo: https://codeberg.org/clarfonthey/testrepo
I ended up realising that LFS wasn't supported by default with `just dev`, so, it ended up working until I made sure the files on the repo *didn't* use LFS.
Assuming you've run `just dev`, you can go directly to this page in the browser here: https://clarfonthey.localhost.mock.directory:4430/testrepo/
And also you can try a few cURL commands:
```shell
curl https://clarfonthey.localhost.mock.directory:4430/testrepo/ --verbose --insecure
curl -H 'Accept-Encoding: gz' https://clarfonthey.localhost.mock.directory:4430/testrepo/ --verbose --insecure | gunzip -
curl -H 'Accept-Encoding: br' https://clarfonthey.localhost.mock.directory:4430/testrepo/ --verbose --insecure | brotli --decompress -
curl -H 'Accept-Encoding: zst' https://clarfonthey.localhost.mock.directory:4430/testrepo/ --verbose --insecure | zstd --decompress -
```
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/387
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: ltdk <usr@ltdk.xyz>
Co-committed-by: ltdk <usr@ltdk.xyz>
2024-09-29 21:00:54 +00:00
if len ( res . CSR ) > 0 {
2021-12-01 21:49:48 +00:00
// CSR stores the time when the renewal shall be tried again
nextTryUnix , err := strconv . ParseInt ( string ( res . CSR ) , 10 , 64 )
if err == nil && time . Now ( ) . Before ( time . Unix ( nextTryUnix , 0 ) ) {
2023-02-10 03:00:14 +00:00
return & tlsCertificate , nil
2021-12-01 21:49:48 +00:00
}
}
2023-02-10 03:00:14 +00:00
// TODO: make a queue ?
2021-11-20 18:36:12 +00:00
go ( func ( ) {
2021-12-01 21:49:48 +00:00
res . CSR = nil // acme client doesn't like CSR to be set
2023-02-11 02:29:08 +00:00
if _ , err := c . obtainCert ( c . legoClient , [ ] string { sni } , res , "" , useDnsProvider , mainDomainSuffix , certDB ) ; err != nil {
2022-11-15 15:15:11 +00:00
log . Error ( ) . Msgf ( "Couldn't renew certificate for %s: %v" , sni , err )
2021-11-20 18:36:12 +00:00
}
} ) ( )
}
}
2023-02-10 03:00:14 +00:00
return & tlsCertificate , nil
2021-11-20 14:54:52 +00:00
}
2023-02-11 02:29:08 +00:00
func ( c * AcmeClient ) obtainCert ( acmeClient * lego . Client , domains [ ] string , renew * certificate . Resource , user string , useDnsProvider bool , mainDomainSuffix string , keyDatabase database . CertDB ) ( * tls . Certificate , error ) {
2021-11-20 14:54:52 +00:00
name := strings . TrimPrefix ( domains [ 0 ] , "*" )
2021-07-13 13:45:28 +00:00
2021-11-20 14:54:52 +00:00
// lock to avoid simultaneous requests
2023-02-11 02:29:08 +00:00
_ , working := c . obtainLocks . LoadOrStore ( name , struct { } { } )
2021-11-20 14:54:52 +00:00
if working {
for working {
time . Sleep ( 100 * time . Millisecond )
2023-02-11 02:29:08 +00:00
_ , working = c . obtainLocks . Load ( name )
2021-11-20 14:54:52 +00:00
}
2023-02-11 02:29:08 +00:00
cert , err := c . retrieveCertFromDB ( name , mainDomainSuffix , useDnsProvider , keyDatabase )
2023-02-10 03:00:14 +00:00
if err != nil {
return nil , fmt . Errorf ( "certificate failed in synchronous request: %w" , err )
2021-11-20 14:54:52 +00:00
}
return cert , nil
}
2023-02-11 02:29:08 +00:00
defer c . obtainLocks . Delete ( name )
2021-11-20 14:54:52 +00:00
2021-12-01 15:23:37 +00:00
if acmeClient == nil {
2024-04-18 17:05:20 +00:00
if useDnsProvider {
return mockCert ( domains [ 0 ] , "DNS ACME client is not defined" , mainDomainSuffix , keyDatabase )
} else {
return mockCert ( domains [ 0 ] , "ACME client uninitialized. This is a server error, please report!" , mainDomainSuffix , keyDatabase )
}
2021-12-01 15:23:37 +00:00
}
2021-11-20 18:36:12 +00:00
// request actual cert
var res * certificate . Resource
var err error
2021-12-01 15:23:37 +00:00
if renew != nil && renew . CertURL != "" {
2023-02-11 02:29:08 +00:00
if c . acmeUseRateLimits {
c . acmeClientRequestLimit . Take ( )
2021-11-20 20:12:28 +00:00
}
2022-08-12 03:06:26 +00:00
log . Debug ( ) . Msgf ( "Renewing certificate for: %v" , domains )
2021-11-20 18:36:12 +00:00
res , err = acmeClient . Certificate . Renew ( * renew , true , false , "" )
2021-12-01 15:23:37 +00:00
if err != nil {
2022-08-12 03:06:26 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "Couldn't renew certificate for %v, trying to request a new one" , domains )
2023-02-11 02:29:08 +00:00
if c . acmeUseRateLimits {
c . acmeClientFailLimit . Take ( )
2023-01-04 05:26:14 +00:00
}
2021-12-01 15:23:37 +00:00
res = nil
}
}
if res == nil {
2021-11-20 20:39:40 +00:00
if user != "" {
2023-02-11 02:29:08 +00:00
if err := c . checkUserLimit ( user ) ; err != nil {
2023-02-10 03:00:14 +00:00
return nil , err
2021-11-20 20:39:40 +00:00
}
}
2023-02-11 02:29:08 +00:00
if c . acmeUseRateLimits {
c . acmeClientOrderLimit . Take ( )
c . acmeClientRequestLimit . Take ( )
2021-11-20 20:12:28 +00:00
}
2022-08-12 03:06:26 +00:00
log . Debug ( ) . Msgf ( "Re-requesting new certificate for %v" , domains )
2021-11-20 18:36:12 +00:00
res , err = acmeClient . Certificate . Obtain ( certificate . ObtainRequest {
Domains : domains ,
Bundle : true ,
MustStaple : false ,
} )
2023-02-11 02:29:08 +00:00
if c . acmeUseRateLimits && err != nil {
c . acmeClientFailLimit . Take ( )
2023-01-04 05:26:14 +00:00
}
2021-11-20 18:36:12 +00:00
}
2021-11-20 14:30:58 +00:00
if err != nil {
2022-08-12 03:06:26 +00:00
log . Error ( ) . Err ( err ) . Msgf ( "Couldn't obtain again a certificate or %v" , domains )
2021-12-01 21:49:48 +00:00
if renew != nil && renew . CertURL != "" {
tlsCertificate , err := tls . X509KeyPair ( renew . Certificate , renew . PrivateKey )
2023-01-04 04:51:27 +00:00
if err != nil {
2023-02-10 03:00:14 +00:00
mockC , err2 := mockCert ( domains [ 0 ] , err . Error ( ) , mainDomainSuffix , keyDatabase )
if err2 != nil {
return nil , errors . Join ( err , err2 )
}
return mockC , err
2023-01-04 04:51:27 +00:00
}
leaf , err := leaf ( & tlsCertificate )
if err == nil && leaf . NotAfter . After ( time . Now ( ) ) {
2024-05-26 20:05:46 +00:00
tlsCertificate . Leaf = leaf
2021-12-01 21:49:48 +00:00
// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
2021-12-01 21:59:52 +00:00
renew . CSR = [ ] byte ( strconv . FormatInt ( time . Now ( ) . Add ( 6 * time . Hour ) . Unix ( ) , 10 ) )
2021-12-05 18:00:57 +00:00
if err := keyDatabase . Put ( name , renew ) ; err != nil {
2023-02-10 03:00:14 +00:00
mockC , err2 := mockCert ( domains [ 0 ] , err . Error ( ) , mainDomainSuffix , keyDatabase )
if err2 != nil {
return nil , errors . Join ( err , err2 )
}
return mockC , err
2021-12-05 18:00:57 +00:00
}
2023-02-10 03:00:14 +00:00
return & tlsCertificate , nil
2021-12-01 21:49:48 +00:00
}
}
2023-02-10 03:00:14 +00:00
return mockCert ( domains [ 0 ] , err . Error ( ) , mainDomainSuffix , keyDatabase )
2021-08-22 15:59:30 +00:00
}
2022-08-12 03:06:26 +00:00
log . Debug ( ) . Msgf ( "Obtained certificate for %v" , domains )
2021-08-22 15:59:30 +00:00
2021-12-05 18:00:57 +00:00
if err := keyDatabase . Put ( name , res ) ; err != nil {
2023-02-10 03:00:14 +00:00
return nil , err
2021-12-05 18:00:57 +00:00
}
2021-11-20 14:30:58 +00:00
tlsCertificate , err := tls . X509KeyPair ( res . Certificate , res . PrivateKey )
2021-07-13 13:45:28 +00:00
if err != nil {
2023-02-10 03:00:14 +00:00
return nil , err
2021-07-13 13:45:28 +00:00
}
2023-02-10 03:00:14 +00:00
return & tlsCertificate , nil
2021-08-22 15:59:30 +00:00
}
2023-02-11 02:29:08 +00:00
func SetupMainDomainCertificates ( mainDomainSuffix string , acmeClient * AcmeClient , certDB database . CertDB ) error {
2021-12-05 22:20:34 +00:00
// getting main cert before ACME account so that we can fail here without hitting rate limits
2022-11-15 15:15:11 +00:00
mainCertBytes , err := certDB . Get ( mainDomainSuffix )
2023-02-10 03:00:14 +00:00
if err != nil && ! errors . Is ( err , database . ErrNotFound ) {
return fmt . Errorf ( "cert database is not working: %w" , err )
2021-11-20 14:54:52 +00:00
}
2021-12-01 15:23:37 +00:00
if mainCertBytes == nil {
2023-02-11 02:29:08 +00:00
_ , err = acmeClient . obtainCert ( acmeClient . dnsChallengerLegoClient , [ ] string { "*" + mainDomainSuffix , mainDomainSuffix [ 1 : ] } , nil , "" , true , mainDomainSuffix , certDB )
2021-11-20 18:36:12 +00:00
if err != nil {
2022-08-12 03:06:26 +00:00
log . Error ( ) . Err ( err ) . Msg ( "Couldn't renew main domain certificate, continuing with mock certs only" )
2021-11-20 18:36:12 +00:00
}
}
2021-12-05 22:20:34 +00:00
return nil
2021-12-05 16:44:10 +00:00
}
2021-11-20 18:36:12 +00:00
2023-02-11 02:29:08 +00:00
func MaintainCertDB ( ctx context . Context , interval time . Duration , acmeClient * AcmeClient , mainDomainSuffix string , certDB database . CertDB ) {
2021-12-05 16:44:10 +00:00
for {
2023-02-10 03:00:14 +00:00
// delete expired certs that will be invalid until next clean up
threshold := time . Now ( ) . Add ( interval )
2021-12-05 16:44:10 +00:00
expiredCertCount := 0
2021-11-20 18:36:12 +00:00
2023-02-10 03:00:14 +00:00
certs , err := certDB . Items ( 0 , 0 )
if err != nil {
log . Error ( ) . Err ( err ) . Msg ( "could not get certs from list" )
} else {
for _ , cert := range certs {
if ! strings . EqualFold ( cert . Domain , strings . TrimPrefix ( mainDomainSuffix , "." ) ) {
if time . Unix ( cert . ValidTill , 0 ) . Before ( threshold ) {
err := certDB . Delete ( cert . Domain )
if err != nil {
log . Error ( ) . Err ( err ) . Msgf ( "Deleting expired certificate for %q failed" , cert . Domain )
} else {
expiredCertCount ++
}
2021-11-20 14:30:58 +00:00
}
}
}
2023-02-10 03:00:14 +00:00
log . Debug ( ) . Msgf ( "Removed %d expired certificates from the database" , expiredCertCount )
2021-12-05 16:44:10 +00:00
}
2021-11-20 14:30:58 +00:00
2021-12-05 16:44:10 +00:00
// update main cert
2022-11-15 15:15:11 +00:00
res , err := certDB . Get ( mainDomainSuffix )
2021-12-05 18:00:57 +00:00
if err != nil {
2022-08-12 03:06:26 +00:00
log . Error ( ) . Msgf ( "Couldn't get cert for domain %q" , mainDomainSuffix )
2021-12-05 18:00:57 +00:00
} else if res == nil {
2022-11-15 15:15:11 +00:00
log . Error ( ) . Msgf ( "Couldn't renew certificate for main domain %q expected main domain cert to exist, but it's missing - seems like the database is corrupted" , mainDomainSuffix )
2021-12-05 16:44:10 +00:00
} else {
tlsCertificates , err := certcrypto . ParsePEMBundle ( res . Certificate )
2023-02-10 03:00:14 +00:00
if err != nil {
log . Error ( ) . Err ( fmt . Errorf ( "could not parse cert for mainDomainSuffix: %w" , err ) )
} else if tlsCertificates [ 0 ] . NotAfter . Before ( time . Now ( ) . Add ( 30 * 24 * time . Hour ) ) {
// renew main certificate 30 days before it expires
2021-12-05 16:44:10 +00:00
go ( func ( ) {
2023-02-11 02:29:08 +00:00
_ , err = acmeClient . obtainCert ( acmeClient . dnsChallengerLegoClient , [ ] string { "*" + mainDomainSuffix , mainDomainSuffix [ 1 : ] } , res , "" , true , mainDomainSuffix , certDB )
2021-12-05 16:44:10 +00:00
if err != nil {
2022-08-12 03:06:26 +00:00
log . Error ( ) . Err ( err ) . Msg ( "Couldn't renew certificate for main domain" )
2021-12-05 16:44:10 +00:00
}
} ) ( )
2021-11-20 14:30:58 +00:00
}
2021-07-13 13:45:28 +00:00
}
2021-12-05 16:44:10 +00:00
2021-12-05 17:26:54 +00:00
select {
case <- ctx . Done ( ) :
return
case <- time . After ( interval ) :
}
2021-12-05 16:44:10 +00:00
}
2021-07-13 13:45:28 +00:00
}
2023-01-04 04:51:27 +00:00
2024-05-26 20:05:46 +00:00
// leaf returns the parsed leaf certificate, either from c.Leaf or by parsing
2023-01-04 04:51:27 +00:00
// the corresponding c.Certificate[0].
2024-05-26 20:05:46 +00:00
// After successfully parsing the cert c.Leaf gets set to the parsed cert.
2023-01-04 04:51:27 +00:00
func leaf ( c * tls . Certificate ) ( * x509 . Certificate , error ) {
if c . Leaf != nil {
return c . Leaf , nil
}
2024-05-26 20:05:46 +00:00
leaf , err := x509 . ParseCertificate ( c . Certificate [ 0 ] )
if err != nil {
return nil , fmt . Errorf ( "tlsCert - failed to parse leaf: %w" , err )
}
c . Leaf = leaf
return leaf , err
2023-01-04 04:51:27 +00:00
}