feat(acme): support EAB credentials

Co-authored-by: techknowlogick <164197+techknowlogick@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-09-30 23:11:43 +00:00
committed by GitHub
parent 1c19e2ba43
commit 6a0e125018
6 changed files with 93 additions and 2 deletions
+17 -1
View File
@@ -21,8 +21,19 @@ import (
"gitea.dev/modules/util"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v3/acme"
)
func acmeExternalAccountBinding() (*acme.EAB, error) {
if setting.AcmeEABKID == "" && setting.AcmeEABHMAC == "" {
return nil, nil
}
if setting.AcmeEABKID == "" || setting.AcmeEABHMAC == "" {
return nil, errors.New("both ACME_EAB_KID and ACME_EAB_HMAC must be set")
}
return &acme.EAB{KeyID: setting.AcmeEABKID, MACKey: setting.AcmeEABHMAC}, nil
}
func getCARoot(path string) (*x509.CertPool, error) {
r, err := os.ReadFile(path)
if err != nil {
@@ -66,6 +77,10 @@ func runACME(listenAddr string, m http.Handler) error {
log.Warn("Failed to parse CA Root certificate, using default CA trust: %v", err)
}
}
externalAccount, err := acmeExternalAccountBinding()
if err != nil {
return err
}
// FIXME: this path is not right, it uses "AppWorkPath" incorrectly, and writes the data into "AppWorkPath/https"
// Ideally it should migrate to AppDataPath write to "AppDataPath/https"
// And one more thing, no idea why we should set the global default variables here
@@ -84,6 +99,7 @@ func runACME(listenAddr string, m http.Handler) error {
Email: setting.AcmeEmail,
Agreed: setting.AcmeTOS,
Profile: setting.AcmeProfile,
ExternalAccount: externalAccount,
DisableHTTPChallenge: !enableHTTPChallenge,
DisableTLSALPNChallenge: !enableTLSALPNChallenge,
ListenHost: setting.HTTPAddr,
@@ -100,7 +116,7 @@ func runACME(listenAddr string, m http.Handler) error {
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
// Prefer keeping the existing cert and retrying renewals asynchronously.
ctx := graceful.GetManager().ShutdownContext()
err := magic.ManageSync(ctx, []string{setting.AppDomain})
err = magic.ManageSync(ctx, []string{setting.AppDomain})
if err != nil {
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.AppDomain)
if cacheErr != nil || cert.Expired() {
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"github.com/mholt/acmez/v3/acme"
"github.com/stretchr/testify/assert"
)
func TestAcmeExternalAccountBinding(t *testing.T) {
t.Cleanup(test.MockVariableValue(&setting.AcmeEABKID, ""))
t.Cleanup(test.MockVariableValue(&setting.AcmeEABHMAC, ""))
binding, err := acmeExternalAccountBinding()
assert.NoError(t, err)
assert.Nil(t, binding)
setting.AcmeEABKID = "kid"
_, err = acmeExternalAccountBinding()
assert.ErrorContains(t, err, "both ACME_EAB_KID and ACME_EAB_HMAC must be set")
setting.AcmeEABHMAC = "hmac"
binding, err = acmeExternalAccountBinding()
assert.NoError(t, err)
assert.Equal(t, &acme.EAB{KeyID: "kid", MACKey: "hmac"}, binding)
}
+5
View File
@@ -264,6 +264,11 @@
;; ACME profile to request from the CA (e.g. "shortlived" for raw-IP certificates)
;ACME_PROFILE =
;;
;; External account binding credentials; set both to enable EAB
;; ACME_EAB_HMAC should be a base64url-encoded MAC key
;ACME_EAB_KID =
;ACME_EAB_HMAC =
;;
;; ACME live directory (not to be confused with ACME directory URL: ACME_URL)
;; (Refer to caddy's ACME manager https://github.com/caddyserver/certmagic)
;ACME_DIRECTORY = https
+1 -1
View File
@@ -68,6 +68,7 @@ require (
github.com/mattn/go-isatty v0.0.24
github.com/mattn/go-sqlite3 v1.14.52
github.com/meilisearch/meilisearch-go v0.36.3
github.com/mholt/acmez/v3 v3.1.6
github.com/mholt/archives v0.1.5
github.com/microcosm-cc/bluemonday v1.0.27
github.com/microsoft/go-mssqldb v1.11.2
@@ -199,7 +200,6 @@ require (
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mholt/acmez/v3 v3.1.6 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
+9
View File
@@ -102,6 +102,8 @@ var (
AcmeEmail string
AcmeURL string
AcmeProfile string
AcmeEABKID string
AcmeEABHMAC string
AcmeCARoot string
SSLMinimumVersion string
SSLMaximumVersion string
@@ -144,6 +146,11 @@ func loadServerDomainAndURL(sec ConfigSection, protocol string) {
AppDomain = appURL.Hostname()
}
func loadAcmeEABFrom(sec ConfigSection) {
AcmeEABKID = sec.Key("ACME_EAB_KID").MustString("")
AcmeEABHMAC = sec.Key("ACME_EAB_HMAC").MustString("")
}
func loadServerFrom(rootCfg ConfigProvider) {
sec := rootCfg.Section("server")
AppName = rootCfg.Section("").Key("APP_NAME").MustString("Gitea: Git with a cup of tea")
@@ -173,6 +180,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
if EnableAcme {
AcmeURL = sec.Key("ACME_URL").MustString("")
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
loadAcmeEABFrom(sec)
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
if sec.HasKey("ACME_ACCEPTTOS") {
@@ -208,6 +216,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
KeyFile = filepath.Join(CustomPath, KeyFile)
}
}
SSLMinimumVersion = sec.Key("SSL_MIN_VERSION").MustString("")
SSLMaximumVersion = sec.Key("SSL_MAX_VERSION").MustString("")
SSLCurvePreferences = sec.Key("SSL_CURVE_PREFERENCES").Strings(",")
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
)
func TestLoadAcmeEABFrom(t *testing.T) {
t.Cleanup(test.MockVariableValue(&AcmeEABKID, ""))
t.Cleanup(test.MockVariableValue(&AcmeEABHMAC, ""))
cfg, err := NewConfigProviderFromData(`
[server]
ACME_EAB_KID = kid
ACME_EAB_HMAC = hmac
`)
assert.NoError(t, err)
loadAcmeEABFrom(cfg.Section("server"))
assert.Equal(t, "kid", AcmeEABKID)
assert.Equal(t, "hmac", AcmeEABHMAC)
}