From 98d198d4197f35af4a50dafab6ff6766eac06faf Mon Sep 17 00:00:00 2001 From: Gusted Date: Wed, 4 Jan 2023 04:51:27 +0000 Subject: [PATCH 1/3] Safely get certificate's leaf (#150) - It's not guaranteed that `tls.X509KeyPair` will set `c.Leaf`. - This patch fixes this by using a wrapper that parses the leaf certificate(in bytes) if `c.Leaf` wasn't set. - Resolves #149 Co-authored-by: Gusted Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/150 Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: Gusted Co-committed-by: Gusted --- server/certificates/certificates.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index 8af4be5..e3cdbfb 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -303,7 +303,11 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains) if renew != nil && renew.CertURL != "" { tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey) - if err == nil && tlsCertificate.Leaf.NotAfter.After(time.Now()) { + if err != nil { + return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err + } + leaf, err := leaf(&tlsCertificate) + if err == nil && leaf.NotAfter.After(time.Now()) { // avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10)) if err := keyDatabase.Put(name, renew); err != nil { @@ -529,3 +533,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi } } } + +// leaf returns the parsed leaf certificate, either from c.leaf or by parsing +// the corresponding c.Certificate[0]. +func leaf(c *tls.Certificate) (*x509.Certificate, error) { + if c.Leaf != nil { + return c.Leaf, nil + } + return x509.ParseCertificate(c.Certificate[0]) +} From f7fad2a5ae0e4942848604a77199f4f1f8a1c3b4 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Wed, 4 Jan 2023 06:08:06 +0100 Subject: [PATCH 2/3] Integration Tests use https://codeberg.org/cb_pages_tests --- integration/get_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/integration/get_test.go b/integration/get_test.go index 81d8488..f13ce8b 100644 --- a/integration/get_test.go +++ b/integration/get_test.go @@ -31,7 +31,7 @@ func TestGetRedirect(t *testing.T) { func TestGetContent(t *testing.T) { log.Println("=== TestGetContent ===") // test get image - resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg") assert.NoError(t, err) if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { t.FailNow() @@ -42,7 +42,7 @@ func TestGetContent(t *testing.T) { assert.Len(t, resp.Header.Get("ETag"), 42) // specify branch - resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/") + resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/") assert.NoError(t, err) if !assert.NotNil(t, resp) { t.FailNow() @@ -53,7 +53,7 @@ func TestGetContent(t *testing.T) { assert.Len(t, resp.Header.Get("ETag"), 44) // access branch name contains '/' - resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/") + resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/") assert.NoError(t, err) if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { t.FailNow() @@ -81,7 +81,7 @@ func TestCustomDomain(t *testing.T) { func TestGetNotFound(t *testing.T) { log.Println("=== TestGetNotFound ===") // test custom not found pages - resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah") assert.NoError(t, err) if !assert.NotNil(t, resp) { t.FailNow() @@ -95,7 +95,7 @@ func TestGetNotFound(t *testing.T) { func TestFollowSymlink(t *testing.T) { log.Printf("=== TestFollowSymlink ===\n") - resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link") assert.NoError(t, err) if !assert.NotNil(t, resp) { t.FailNow() @@ -111,7 +111,7 @@ func TestFollowSymlink(t *testing.T) { func TestLFSSupport(t *testing.T) { log.Printf("=== TestLFSSupport ===\n") - resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") + resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") assert.NoError(t, err) if !assert.NotNil(t, resp) { t.FailNow() From c286b3b1d0a21bc5cc447df7744da9894d857e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Leopoldo=20Sologuren=20Guti=C3=A9rrez?= Date: Wed, 4 Jan 2023 05:26:14 +0000 Subject: [PATCH 3/3] Added TokenBucket to limit the rate of validation failures (#151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new TockenBucket named `acmeClientFailLimit` to avoid being banned because of the [Failed validation limit](https://letsencrypt.org/docs/failed-validation-limit/) of Let's Encrypt. The behaviour is similar to the other limiters blocking the `obtainCert` func ensuring rate under limit. Co-authored-by: fsologureng Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/151 Reviewed-by: 6543 <6543@obermui.de> Co-authored-by: Felipe Leopoldo Sologuren GutiƩrrez Co-committed-by: Felipe Leopoldo Sologuren GutiƩrrez --- server/certificates/certificates.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/certificates/certificates.go b/server/certificates/certificates.go index e3cdbfb..1aa90a0 100644 --- a/server/certificates/certificates.go +++ b/server/certificates/certificates.go @@ -163,6 +163,9 @@ var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute) // rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests) var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second) +// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/ +var acmeClientFailLimit = equalizer.NewTokenBucket(5, 1*time.Hour) + type AcmeTLSChallengeProvider struct { challengeCache cache.SetGetKey } @@ -278,6 +281,9 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re res, err = acmeClient.Certificate.Renew(*renew, true, false, "") if err != nil { log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains) + if acmeUseRateLimits { + acmeClientFailLimit.Take() + } res = nil } } @@ -298,6 +304,9 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re Bundle: true, MustStaple: false, }) + if acmeUseRateLimits && err != nil { + acmeClientFailLimit.Take() + } } if err != nil { log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains)