diff --git a/README.md b/README.md index 9f7fad6..14ac305 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Configuration variables: - `ACME_DNS_PROPAGATION_REQUIREMENT` - if set to true, requires complete DNS record propagation before stating that challenge is solved. Default: true - `ACME_REREGISTER_ACCOUNT` - if set to true, allows registering an account with CA. This should be set to true for the first use. When credentials are stored in Vault, you can set this to false to avoid accidental registrations. Default: false - `ACME_SERVER_URL` - ACME directory location. Default: https://acme-staging-v02.api.letsencrypt.org/directory +- `ACME_EAB_KID` - ACME External Account Binding Key Identifier. Optional. +- `ACME_EAB_HMAC_KEY` - ACME External Account Binding HMAC Key. Optional. - `VAULT_APPROLE_ROLE_ID` - role ID for Vault approle authentication method. **Required in prod env** - `VAULT_APPROLE_SECRET_ID` - secret ID for Vault approle authentication method. **Required in prod env** - `VAULT_KV_STORAGE_PATH` - path in Vault KV storage where certificator stores certificates and account data. Default: secret/data/certificator/ diff --git a/cmd/certificator/main.go b/cmd/certificator/main.go index 26910d2..6ee6f9c 100644 --- a/cmd/certificator/main.go +++ b/cmd/certificator/main.go @@ -46,8 +46,7 @@ func main() { logger.Fatal(err) } - acmeClient, err := acme.NewClient(cfg.Acme.AccountEmail, cfg.Acme.ServerURL, - cfg.Acme.ReregisterAccount, vaultClient, logger) + acmeClient, err := acme.NewClient(cfg.Acme, vaultClient, logger) if err != nil { logger.Fatal(err) } diff --git a/docker-compose.yml b/docker-compose.yml index b78c514..6ff3170 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,10 +6,17 @@ services: # ports: # - "14000:14000" # HTTPS ACME API # - "15000:15000" # HTTPS Management API + pebble-eab: + image: letsencrypt/pebble:latest + command: pebble -config /test/config/pebble-config-external-account-bindings.json -strict -dnsserver challtestsrv:8053 + volumes: + - ./fixtures/pebble-eab/cert.pem:/test/certs/localhost/cert.pem:Z + - ./fixtures/pebble-eab/key.pem:/test/certs/localhost/key.pem:Z challtestsrv: image: letsencrypt/pebble-challtestsrv:latest depends_on: - pebble + - pebble-eab command: pebble-challtestsrv -http01 "" -tlsalpn01 "" -dns01 ":8053" ports: - "8055:8055" # HTTP Management API @@ -25,6 +32,7 @@ services: build: . depends_on: - pebble + - pebble-eab - challtestsrv - vault env_file: @@ -36,6 +44,7 @@ services: dockerfile: Dockerfile.tester depends_on: - pebble + - pebble-eab - challtestsrv - vault command: ["true"] # do not start the container when `docker-compose up` is executed diff --git a/fixtures/pebble-eab/cert.pem b/fixtures/pebble-eab/cert.pem new file mode 100644 index 0000000..62a3b5d --- /dev/null +++ b/fixtures/pebble-eab/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUDMCvZoyIm0luDchyjAX438QRODEwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAxMVbWluaWNhIHJvb3QgY2EgMjRlMmRiMB4XDTI1MTExMzE3 +NTAxM1oXDTM1MTExMTE3NTAxM1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmuh8Ij6woMEinHbutHHf8M6QK8RV +mYwxDrZsS786FbNruOkxGMTSEsTGXsWZQl++WrGnNnbau6BdY2Na7eS+CRu/POZr +sV/mCFsqoCcDCdMdl8cKqKRdkn4jz7BOAVdZMFvBkZMlJLnzmXajMCkceHfDP9M7 +PiwceLkkJ5stnfsdNna3BMdDQXwwNHGBG+ENI707rs25arT9L0+dLD4gXNUBFPaH +/myMMnFZrE4Wh9TasI/1lqcmsWrA+CAUvt1SGqlfWZfAMrNp+u1KdESp+CfwI+0F +y0KJ+LmIMWVEeTJiE1IaXxiwNXLndNODl6shQ92bKQAn2PPR/f3mf9/epwIDAQAB +o4G0MIGxMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF +BwMBMCgGA1UdEQQhMB+CCWxvY2FsaG9zdIIGcGViYmxlggpwZWJibGUtZWFiMB0G +A1UdDgQWBBQHS0Usia66JfBmckioDedWLYYQWDA5BgNVHSMEMjAwoSSkIjAgMR4w +HAYDVQQDExVtaW5pY2Egcm9vdCBjYSAyNGUyZGKCCCTi23rPLE6+MA0GCSqGSIb3 +DQEBCwUAA4IBAQBG0K1AnLPBL1yowiMUoGYpB8SI6J+yTuFDo582C9eJxhJG5TTH +AgTv/Ily/wU805yhw1afyq/eNomECMybrJ83DukQGNLoWjbqCLlZNo7ngyy7VeOA +jMiDgzAbQzLmhJqjOOa4SORWxMOIokp0wgrV3eB9OyxuxKmgio5MuCP5xZY57Hxt +HS3WBzHf+OqmD8obwrPdkrB3wxosiNAs96wsXCgExdOK8lWvZqC+3jId+EadSdCU +hGhEeMfhkn19o+5W+dhCdpWHMCtus3/X5lwo+RGlLTn9tB+7DLp/RT1UIanQgxT0 +f5jAWybkXoSSj1AOJupCojLailyLQeqRs1eC +-----END CERTIFICATE----- diff --git a/fixtures/pebble-eab/key.pem b/fixtures/pebble-eab/key.pem new file mode 100644 index 0000000..013383f --- /dev/null +++ b/fixtures/pebble-eab/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCa6HwiPrCgwSKc +du60cd/wzpArxFWZjDEOtmxLvzoVs2u46TEYxNISxMZexZlCX75asac2dtq7oF1j +Y1rt5L4JG7885muxX+YIWyqgJwMJ0x2XxwqopF2SfiPPsE4BV1kwW8GRkyUkufOZ +dqMwKRx4d8M/0zs+LBx4uSQnmy2d+x02drcEx0NBfDA0cYEb4Q0jvTuuzblqtP0v +T50sPiBc1QEU9of+bIwycVmsThaH1Nqwj/WWpyaxasD4IBS+3VIaqV9Zl8Ays2n6 +7Up0RKn4J/Aj7QXLQon4uYgxZUR5MmITUhpfGLA1cud004OXqyFD3ZspACfY89H9 +/eZ/396nAgMBAAECggEABoOoVLhWLSxyDCpkN2pihHnrTivffamLlpbNTxInmyoE +aa5Exkngm7FytiGRJF1qX94AriWpZSKFStyuwjpQU4m7LRBzju39enR7Amkhk5Qx +NmTEafsfhD2Ru/YqEZxev83cAgcyFK1NNTLsAeZ4sAg5ScHUR/UK7Xi7sH6yGGaa +OfNZf+2JXDSIQhZ5wKfP9UMLw7Pnk/tiTptWvAhhssHzUjeTZWOB0YcNLtdjMry0 +a4Vgo1VEnTGlaazBRZrphcnkYz0L9TdZi3MBowFpwSAVaFnIYBVj0yMzUob7wMR4 +rQ13oDB0cY6diSSBBcYAdcU5tl3Rgwe674M5r47OYQKBgQDSKNgsPTB5SWF4XPqR +pbzpoD/OCT9JWs2KfubkaYjT7W8vKt1a1+0NplcO8v9fAFaVraLWz2QWBcgBA2AT +3P969RRwq5RozujpKtlaq3np5sx6zLOV/EraCTrBAWG7bhnQ45NiuOZ0o4twn0h+ +bn6cOg9WmAv5I6yUrp453OKdjQKBgQC8snFDgjK4t0dC7j83n5wX5U0H9Rl3IdXF +u2ccZSiw1ZuAh14iqEWbD404tqdUIJUDOb/Gp4C/cXaqGPX5+aD8m8o725Sxd4wy +htSr+Y2+t6RrRLjP3gmHwkGdacMLe0i8JSR1uv7mTCO9c6KZMuzG0wfoFmKLhH1N +uo1DmaeeAwKBgDa9+ukzGyBMs/31QjOUF2xHmLcjg3+pDmcn/R68Oq7xZwfDQ5t5 +KT9n979mHozxepxMeWE47loqMVnKNkmWRFoT4roih2rxkovB5JIQnpYFz3ehQJot ++JVo2AwUPw/icwkNboPFpAn328d/RhPg1kv9d6dXva6s0zR7c62g0sq1AoGATTLD +29acYa253cwS2PLXrX8GMMc5j9+r0aWF3bwVFaOOZtr6zDsDnxiRsi4n7ucVj6hE +qUVGCwyke5qSp55VGoOyDSMchi5ekR1QTLtgHjKLdcc7xO8V/xCeP4RRY5UggLjB +XdkPB7EGvi9cykhnTkBv76eY0XN2wCIeQRXP2XkCgYAxF8s7syOR9ViPPMKxH/xM +GULvgMmXq4qvz+mi/n1Tnu/7NJPy34SR+nZvGdaKXwng2cCa8qlUrHlQod1hPsaT +6L8TT/4umoNGJdujqjjLML5reeqRQJGrQiULhZAajYGXTAKi84H2YPcK+60mFcKg +j27RTCjgFLvRubrM2fpgEg== +-----END PRIVATE KEY----- diff --git a/pkg/acme/acme.go b/pkg/acme/acme.go index 1143ef6..8dafe8c 100644 --- a/pkg/acme/acme.go +++ b/pkg/acme/acme.go @@ -12,6 +12,7 @@ import ( "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" "github.com/sirupsen/logrus" + "github.com/vinted/certificator/pkg/config" "github.com/vinted/certificator/pkg/vault" ) @@ -40,22 +41,21 @@ func (u *User) GetPrivateKey() crypto.PrivateKey { // NewClient initializes acme client and returns func NewClient( - email, serverURL string, - reregister bool, + cfg config.Acme, vault *vault.VaultClient, logger *logrus.Logger) (*lego.Client, error) { - acc, err := setupAccount(email, reregister, vault, logger) + acc, err := setupAccount(cfg.AccountEmail, cfg.ReregisterAccount, vault, logger) if err != nil { return nil, err } - client, err := setupClient(acc, serverURL, logger) + client, err := setupClient(acc, cfg.ServerURL, logger) if err != nil { return nil, err } - return registerAccount(acc, client, vault, serverURL, reregister, logger) + return registerAccount(acc, client, vault, cfg, logger) } func setupClient( @@ -156,13 +156,13 @@ func getAccountKey(reregister bool, vault *vault.VaultClient, logger *logrus.Log } func registerAccount(acc *User, client *lego.Client, vault *vault.VaultClient, - serverURL string, reregister bool, logger *logrus.Logger) (*lego.Client, error) { + cfg config.Acme, logger *logrus.Logger) (*lego.Client, error) { logger.Debug("checking client registration") _, err := client.Registration.QueryRegistration() if err != nil { logger.Warn("registration not found") - client, err = recoverAccount(acc, client, vault, serverURL, reregister, logger) + client, err = recoverAccount(acc, client, vault, cfg, logger) if err != nil { return nil, err } @@ -174,26 +174,34 @@ func registerAccount(acc *User, client *lego.Client, vault *vault.VaultClient, } func recoverAccount(acc *User, client *lego.Client, vault *vault.VaultClient, - serverURL string, reregister bool, logger *logrus.Logger) (*lego.Client, error) { + cfg config.Acme, logger *logrus.Logger) (*lego.Client, error) { // Try to resolve registration by private key reg, err := client.Registration.ResolveAccountByKey() if err != nil { logger.Warn("could not resolve account by key") - if reregister { + if cfg.ReregisterAccount { // Reset local registration data and reregister logger.Info("reregistering account") acc.Registration = nil - client, err = setupClient(acc, serverURL, logger) + client, err = setupClient(acc, cfg.ServerURL, logger) if err != nil { return nil, err } - reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - return nil, err + if cfg.EABKid != "" && cfg.EABHmacKey != "" { + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{TermsOfServiceAgreed: true, Kid: cfg.EABKid, HmacEncoded: cfg.EABHmacKey}) + if err != nil { + return nil, err + } + } else { + reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + return nil, err + } } + acc.Registration = reg } else { return nil, errors.New("account registration not found and re-registering is disabled") diff --git a/pkg/config/config.go b/pkg/config/config.go index 2a28595..d0bb26e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,6 +16,8 @@ type Acme struct { DNSPropagationRequirement bool `envconfig:"ACME_DNS_PROPAGATION_REQUIREMENT" default:"true"` ReregisterAccount bool `envconfig:"ACME_REREGISTER_ACCOUNT" default:"false"` ServerURL string `envconfig:"ACME_SERVER_URL" default:"https://acme-staging-v02.api.letsencrypt.org/directory"` + EABKid string `envconfig:"ACME_EAB_KID"` + EABHmacKey string `envconfig:"ACME_EAB_HMAC_KEY"` } // Vault contains vault related configuration parameters diff --git a/test/integration_test.go b/test/integration_test.go index 5ed4e52..efea96b 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -11,18 +11,23 @@ import ( "github.com/thanos-io/thanos/pkg/testutil" "github.com/vinted/certificator/pkg/acme" "github.com/vinted/certificator/pkg/certificate" + "github.com/vinted/certificator/pkg/config" "github.com/vinted/certificator/pkg/vault" ) var ( // vaultDevToken token should be equal to `VAULT_DEV_ROOT_TOKEN_ID` set in vault container // It should be defined in docker-compoose.yml - vaultDevToken string = "supersecret" - vaultKVPath string = "/secret/data/integration_test/" - acc *acme.User - keyEncoded string - acmeEmail string = "test@test.com" - acmeURL string = "https://pebble:14000/dir" + vaultDevToken string = "supersecret" + vaultKVPath string = "/secret/data/integration_test/" + vaultEABKVPath string = "/secret/data/integration_test_eab/" + acc *acme.User + keyEncoded string + acmeEmail string = "test@test.com" + acmeURL string = "https://pebble:14000/dir" + acmeEABURL string = "https://pebble-eab:14000/dir" + acmeEABKID string = "kid-1" + acmeEABHMACKEY string = "zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W" ) func TestMain(m *testing.M) { @@ -55,8 +60,14 @@ func TestAcmeClientAndAccountSetup(t *testing.T) { deleteAccountFromVault(t, testVaultClient) deleteKeyFromVault(t, testVaultClient) + acmeConfig := config.Acme{ + AccountEmail: acmeEmail, + ServerURL: acmeURL, + ReregisterAccount: true, + } + // This populates data in Vault, account and key are both present - _, err = acme.NewClient(acmeEmail, acmeURL, true, vaultClient, logger) + _, err = acme.NewClient(acmeConfig, vaultClient, logger) testutil.Ok(t, err) // Save account and key data from first registration @@ -132,7 +143,12 @@ func TestAcmeClientAndAccountSetup(t *testing.T) { testutil.Ok(t, err) } - _, err := acme.NewClient(acmeEmail, acmeURL, tcase.reregisteringEnabled, vaultClient, logger) + acmeConfig := config.Acme{ + AccountEmail: acmeEmail, + ServerURL: acmeURL, + ReregisterAccount: tcase.reregisteringEnabled, + } + _, err := acme.NewClient(acmeConfig, vaultClient, logger) if tcase.expectedErr { testutil.NotOk(t, err) } else { @@ -149,7 +165,12 @@ func TestCertificateObtaining(t *testing.T) { vaultClient, err := vault.NewVaultClient("", "", "dev", vaultKVPath, logger) testutil.Ok(t, err) - acmeClient, err := acme.NewClient(acmeEmail, acmeURL, true, vaultClient, logger) + acmeConfig := config.Acme{ + AccountEmail: acmeEmail, + ServerURL: acmeURL, + ReregisterAccount: true, + } + acmeClient, err := acme.NewClient(acmeConfig, vaultClient, logger) testutil.Ok(t, err) for _, domain := range []string{"example.com", "test.com", "mydomain.com"} { @@ -165,6 +186,36 @@ func TestCertificateObtaining(t *testing.T) { } } +func TestCertificateObtainingWithEAB(t *testing.T) { + logger := logrus.New() + logger.SetLevel(logrus.WarnLevel) + + vaultClient, err := vault.NewVaultClient("", "", "dev", vaultEABKVPath, logger) + testutil.Ok(t, err) + + acmeConfig := config.Acme{ + ServerURL: acmeEABURL, + ReregisterAccount: true, + EABKid: acmeEABKID, + EABHmacKey: acmeEABHMACKEY, + } + + acmeClient, err := acme.NewClient(acmeConfig, vaultClient, logger) + testutil.Ok(t, err) + + for _, domain := range []string{"example-eab.com", "test-eab.com", "mydomain-eab.com"} { + err := certificate.ObtainCertificate(acmeClient, vaultClient, []string{domain}, + "challtestsrv:8053", "exec", false) + testutil.Ok(t, err) + + cert, err := certificate.GetCertificate(domain, vaultClient) + testutil.Ok(t, err) + + // Check if certificate is issued recently + testutil.Assert(t, time.Since(cert.NotBefore).Minutes() < 5) + } +} + func deleteAccountFromVault(t *testing.T, cl *api.Client) { t.Log("Deleting account from Vault") _, err := cl.Logical().Delete(vaultKVPath + "account")