From 2a4b1223480e467cf6b961514bc9bc11c05f01b6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:23:17 -0800 Subject: [PATCH 01/80] Finish infra for sending raw ACME req, add tests for nonce --- backend/bdd/features/pki/acme/account.feature | 1 + .../features/pki/acme/cert-profile.feature | 6 +-- .../bdd/features/pki/acme/challenge.feature | 2 - .../bdd/features/pki/acme/dicrectory.feature | 2 +- backend/bdd/features/pki/acme/nonce.feature | 24 +++++++++- backend/bdd/features/steps/pki_acme.py | 45 ++++++++++++++++++- 6 files changed, 71 insertions(+), 9 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 7e1d67a93..60400cff0 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -4,3 +4,4 @@ Feature: Account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 7ce1ecc8c..2a325c7c1 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -3,7 +3,7 @@ Feature: ACME Cert Profile Scenario: Create a cert profile Given I make a random slug as profile_slug Given I use AUTH_TOKEN for authentication - When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -25,7 +25,7 @@ Feature: ACME Cert Profile Scenario: Reveal EAB secret Given I make a random slug as profile_slug Given I use AUTH_TOKEN for authentication - When I send a POST request to "/api/v1/pki/certificate-profiles" with JSON payload + When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload """ { "projectId": "{PROJECT_ID}", @@ -39,7 +39,7 @@ Feature: ACME Cert Profile """ Then the value response.status_code should be equal to 200 And I memorize response with jq ".certificateProfile.id" as profile_id - When I send a GET request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then the value response.status_code should be equal to 200 Then the value response with jq ".eabKid" should be equal to "{profile_id}" Then the value response with jq ".eabSecret" should be present diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index ba9970e43..ece895848 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -3,7 +3,6 @@ Feature: Challenge Scenario: Validate challenge Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -20,4 +19,3 @@ Feature: Challenge Then I tell ACME server that challenge is ready to be verified Then I poll and finalize the ACME order order as finalized_order Then the value finalized_order.body with jq ".status" should be equal to "valid" - # TODO: check the fullchain pem content of the order diff --git a/backend/bdd/features/pki/acme/dicrectory.feature b/backend/bdd/features/pki/acme/dicrectory.feature index 481a3337a..dbb980ac3 100644 --- a/backend/bdd/features/pki/acme/dicrectory.feature +++ b/backend/bdd/features/pki/acme/dicrectory.feature @@ -2,7 +2,7 @@ Feature: Directory Scenario: Get the directory of ACME service urls Given I have an ACME cert profile as "acme_profile" - When I send a GET request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then the response status code should be "200" Then the response body should match JSON value """ diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 7bbeb3b9d..e7325a12b 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -2,6 +2,28 @@ Feature: Nonce Scenario: Generate a new nonce Given I have an ACME cert profile as "acme_profile" - When I send a HEAD request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" + When I send a "HEAD" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" Then the response status code should be "200" Then the response header "Replay-Nonce" should contains non-empty value + + Scenario: Send bad nonce + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id + When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + """ + { + "protected": { + "alg": "RS256", + "nonce": "oFvnlFP1wIhRlYS2jTaXbA", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" + Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".detail" should be equal to "Invalid nonce" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index d0fa627cd..be995a2cf 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -3,7 +3,9 @@ import logging import os import re import threading +import urllib.parse +import acme.client import httpx import jq import requests @@ -12,12 +14,14 @@ from faker import Faker from acme import client from acme import messages from acme import standalone +from acme.jws import Signature from behave.runner import Context from behave import given from behave import when from behave import then from josepy.jwk import JWKRSA from josepy import JSONObjectWithFields +from josepy import json_util from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography import x509 @@ -152,7 +156,7 @@ def step_impl(context: Context, token_var: str): context.auth_token = eval_var(context, token_var) -@when('I send a {method} request to "{url}"') +@when('I send a "{method}" request to "{url}"') def step_impl(context: Context, method: str, url: str): logger.debug("Sending %s request to %s", method, url) response = context.http_client.request( @@ -166,7 +170,7 @@ def step_impl(context: Context, method: str, url: str): pass -@when('I send a {method} request to "{url}" with JSON payload') +@when('I send a "{method}" request to "{url}" with JSON payload') def step_impl(context: Context, method: str, url: str): json_payload = json.loads(context.text) json_payload = replace_vars(json_payload, context.vars) @@ -248,6 +252,43 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: context.vars[account_var] = acme_client.new_account(registration) +def send_raw_acme_req(context: Context, url: str): + acme_client = context.acme_client + content = json.loads(context.text) + protected = replace_vars(content["protected"], context.vars) + payload = ( + replace_vars(content["payload"], context.vars) if "payload" in content else None + ) + alg = acme_client.net.alg + encoded_payload = json.dumps(payload).encode() if payload else b"" + protected_headers = json.dumps(protected) + signature = alg.sign( + key=acme_client.net.key.key, + msg=Signature._msg(protected_headers, encoded_payload), + ) + jws = json.dumps( + { + "protected": json_util.encode_b64jose(protected_headers.encode()), + "payload": json_util.encode_b64jose(encoded_payload), + "signature": json_util.encode_b64jose(signature), + } + ) + base_url = context.vars["BASE_URL"] + url = urllib.parse.urljoin(base_url, replace_vars(url, context.vars)) + response = acme_client.net._send_request( + "POST", + url, + data=jws, + headers={"Content-Type": acme.client.ClientNetwork.JOSE_CONTENT_TYPE}, + ) + context.vars["response"] = response + + +@when('I send a raw ACME request to "{url}"') +def step_impl(context: Context, url: str): + send_raw_acme_req(context, url) + + @then( "I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}" ) From d1dad08005485f0a0fa6236c152378c6956ed141 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:32:17 -0800 Subject: [PATCH 02/80] Send nonce twice --- backend/bdd/features/pki/acme/nonce.feature | 38 ++++++++++++++++++++- backend/bdd/features/steps/pki_acme.py | 6 ++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index e7325a12b..5475b9cfd 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -6,7 +6,7 @@ Feature: Nonce Then the response status code should be "200" Then the response header "Replay-Nonce" should contains non-empty value - Scenario: Send bad nonce + Scenario: Send a bad nonce Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account @@ -27,3 +27,39 @@ Feature: Nonce Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".status" should be equal to 400 Then the value response with jq ".detail" should be equal to "Invalid nonce" + + Scenario: Send the same nonce twice + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id + Then I peak and memorize the next nonce as nonce_value + When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce_value}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 200 + When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce_value}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" + Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".detail" should be equal to "Invalid nonce" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index be995a2cf..e5acae427 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -439,6 +439,12 @@ def step_impl(context: Context, var_path: str, jq_query, var_name: str): context.vars[var_name] = value +@then("I peak and memorize the next nonce as {var_name}") +def step_impl(context: Context, var_name: str): + acme_client = context.acme_client + context.vars[var_name] = json_util.encode_b64jose(list(acme_client.net._nonces)[0]) + + @then("I memorize {var_path} as {var_name}") def step_impl(context: Context, var_path: str, var_name: str): value = eval_var(context, var_path) From dc00fb366460135905b935a56fbfec27cc0cc861 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:35:07 -0800 Subject: [PATCH 03/80] Fix tests --- backend/bdd/features/steps/pki_acme.py | 2 +- backend/src/ee/services/pki-acme/pki-acme-service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index e5acae427..9abe74ab4 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -260,7 +260,7 @@ def send_raw_acme_req(context: Context, url: str): replace_vars(content["payload"], context.vars) if "payload" in content else None ) alg = acme_client.net.alg - encoded_payload = json.dumps(payload).encode() if payload else b"" + encoded_payload = json.dumps(payload).encode() if payload is not None else b"" protected_headers = json.dumps(protected) signature = alg.sign( key=acme_client.net.key.key, diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 1e7123353..09faaf8bc 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -199,7 +199,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); } logger.error(error, "Unexpected error while parsing JWS payload"); - throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); + throw new AcmeMalformedError({ detail: "Failed to verify JWS payload" }); } }; From cac6059cd96a4b9e0e58da21d78816d7c94ed92c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:45:23 -0800 Subject: [PATCH 04/80] Cover more endpoints --- backend/bdd/features/pki/acme/nonce.feature | 22 ++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 5475b9cfd..5ae3ab1f8 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -6,18 +6,28 @@ Feature: Nonce Then the response status code should be "200" Then the response header "Replay-Nonce" should contains non-empty value - Scenario: Send a bad nonce + Scenario Outline: Send a bad nonce to account endpoints Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id - When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + When I send a raw ACME request to "" """ { "protected": { "alg": "RS256", "nonce": "oFvnlFP1wIhRlYS2jTaXbA", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "url": "", "kid": "{acme_account.uri}" }, "payload": {} @@ -28,6 +38,12 @@ Feature: Nonce Then the value response with jq ".status" should be equal to 400 Then the value response with jq ".detail" should be equal to "Invalid nonce" + Examples: Endpoints + | path | + | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | {order.uri} | + Scenario: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory From c509697b8db0d5f747e7db8dd0f9e4ae46576acc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:50:30 -0800 Subject: [PATCH 05/80] More test cases --- backend/bdd/features/pki/acme/nonce.feature | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 5ae3ab1f8..e337949d9 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -21,6 +21,7 @@ Feature: Nonce Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then I memorize with jq "" as When I send a raw ACME request to "" """ { @@ -39,10 +40,12 @@ Feature: Nonce Then the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | path | - | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | - | {order.uri} | + | src_var | jq | dest_var | path | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | .authorizations[0] | auth_uri | {auth_uri} | Scenario: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" From 806421ab9df2a413795aa9b8e4e58bd67759a80e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:53:28 -0800 Subject: [PATCH 06/80] Fix tests --- backend/bdd/features/pki/acme/nonce.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index e337949d9..f0f4cdee7 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -40,12 +40,12 @@ Feature: Nonce Then the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | path | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | .authorizations[0] | auth_uri | {auth_uri} | + | src_var | jq | dest_var | path | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | Scenario: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" From 98c4d69870270e934120f0f26cdcd960e3938e4d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 17:59:53 -0800 Subject: [PATCH 07/80] More test cases --- backend/bdd/features/pki/acme/nonce.feature | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index f0f4cdee7..1c76d51f7 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -40,12 +40,13 @@ Feature: Nonce Then the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | path | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | src_var | jq | dest_var | path | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | Scenario: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" From 78047fdbaa6b2de517b5b619f5a2a01f7a26e20a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 18:03:02 -0800 Subject: [PATCH 08/80] Cover all --- backend/bdd/features/pki/acme/nonce.feature | 1 + backend/bdd/features/steps/pki_acme.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 1c76d51f7..c3ec59ac4 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -45,6 +45,7 @@ Feature: Nonce | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | | order | . | not_used | {order.uri} | | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | | order | .authorizations[0].uri | auth_uri | {auth_uri} | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 9abe74ab4..02148b287 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -274,10 +274,10 @@ def send_raw_acme_req(context: Context, url: str): } ) base_url = context.vars["BASE_URL"] - url = urllib.parse.urljoin(base_url, replace_vars(url, context.vars)) + actual_url = urllib.parse.urljoin(base_url, replace_vars(url, context.vars)) response = acme_client.net._send_request( "POST", - url, + actual_url, data=jws, headers={"Content-Type": acme.client.ClientNetwork.JOSE_CONTENT_TYPE}, ) From 883458782658511ba683cb30eb3a465bd1f73bac Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 18:07:52 -0800 Subject: [PATCH 09/80] Make nonce endpoint works --- backend/bdd/features/pki/acme/nonce.feature | 27 ++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index c3ec59ac4..d92770d2e 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -49,11 +49,21 @@ Feature: Nonce | order | .authorizations[0].uri | auth_uri | {auth_uri} | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | - Scenario: Send the same nonce twice + Scenario Outline: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I peak and memorize the next nonce as nonce_value When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" """ @@ -68,13 +78,14 @@ Feature: Nonce } """ Then the value response.status_code should be equal to 200 - When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" + Then I memorize with jq "" as + When I send a raw ACME request to "" """ { "protected": { "alg": "RS256", "nonce": "{nonce_value}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "url": "", "kid": "{acme_account.uri}" }, "payload": {} @@ -84,3 +95,13 @@ Feature: Nonce Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".status" should be equal to 400 Then the value response with jq ".detail" should be equal to "Invalid nonce" + + Examples: Endpoints + | src_var | jq | dest_var | path | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | From 9e12d67e01e80b8b381d84d59d9115d713ea93f9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 18:14:36 -0800 Subject: [PATCH 10/80] EAB tests --- backend/bdd/features/pki/acme/account.feature | 6 ++++++ backend/bdd/features/steps/pki_acme.py | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 60400cff0..1540aa1df 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -5,3 +5,9 @@ Feature: Account When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) + + Scenario: Create a new account without EAB + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com without EAB + Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 02148b287..2e0596666 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -252,6 +252,18 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: context.vars[account_var] = acme_client.new_account(registration) +@then("I register a new ACME account with email {email} without EAB") +def step_impl(context: Context, email: str): + acme_client = context.acme_client + registration = messages.NewRegistration.from_data( + email=email, + ) + try: + acme_client.new_account(registration) + except Exception as exp: + context.vars["error"] = exp + + def send_raw_acme_req(context: Context, url: str): acme_client = context.acme_client content = json.loads(context.text) From 3246b80c81475dffe110cc43c5ac59b2da9e04cb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 18:24:07 -0800 Subject: [PATCH 11/80] More test cases --- backend/bdd/features/pki/acme/account.feature | 13 +++++++++++++ backend/bdd/features/steps/pki_acme.py | 7 +++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 1540aa1df..12000b35c 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -11,3 +11,16 @@ Feature: Account When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com without EAB Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" + + Scenario Outline: Scenario: Create a new account with bad EAB credentials + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account + Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:malformed" + Then the value error with jq ".detail" should be equal to "Invalid external account binding JWS signature" + + Examples: Bad Credentials + | eab_kid | eab_secret | + | bad | Cg== | + | {acme_profile.eab_kid} | Cg== | + | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 2e0596666..c07a712de 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -249,7 +249,10 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: email=email, external_account_binding=eab, ) - context.vars[account_var] = acme_client.new_account(registration) + try: + context.vars[account_var] = acme_client.new_account(registration) + except Exception: + context.vars["error"] = acme_client.new_account(registration) @then("I register a new ACME account with email {email} without EAB") @@ -259,7 +262,7 @@ def step_impl(context: Context, email: str): email=email, ) try: - acme_client.new_account(registration) + context.vars["error"] = acme_client.new_account(registration) except Exception as exp: context.vars["error"] = exp From 159eb3138de4df5c6aab8ed1bef7a174261778fd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 18:27:41 -0800 Subject: [PATCH 12/80] More tests --- backend/bdd/features/pki/acme/account.feature | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 12000b35c..5082155a9 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -22,5 +22,7 @@ Feature: Account Examples: Bad Credentials | eab_kid | eab_secret | | bad | Cg== | + | bad | Cg== | | {acme_profile.eab_kid} | Cg== | + | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | From f11c4084ace29b73e90c741e716fc3241d3d7120 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:03:10 -0800 Subject: [PATCH 13/80] Detail and message are mostly the same for acme error, use just one to avoid conufsing --- backend/bdd/features/pki/acme/account.feature | 16 +- backend/bdd/features/steps/pki_acme.py | 4 +- .../ee/services/pki-acme/pki-acme-errors.ts | 279 +++++++----------- .../src/ee/services/pki-acme/pki-acme-fns.ts | 2 +- .../ee/services/pki-acme/pki-acme-service.ts | 46 +-- backend/src/server/plugins/error-handler.ts | 3 +- 6 files changed, 144 insertions(+), 206 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 5082155a9..5685167b1 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -16,13 +16,13 @@ Feature: Account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account - Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:malformed" - Then the value error with jq ".detail" should be equal to "Invalid external account binding JWS signature" + Then the value error with jq ".type" should be equal to "" + Then the value error with jq ".detail" should be equal to "" Examples: Bad Credentials - | eab_kid | eab_secret | - | bad | Cg== | - | bad | Cg== | - | {acme_profile.eab_kid} | Cg== | - | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | - | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | + | eab_kid | eab_secret | error_type | error_msg | + | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | + | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | + | {acme_profile.eab_kid} | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | + | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | + | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | fixme | diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index c07a712de..0a753a827 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -251,8 +251,8 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: ) try: context.vars[account_var] = acme_client.new_account(registration) - except Exception: - context.vars["error"] = acme_client.new_account(registration) + except Exception as exp: + context.vars["error"] = exp @then("I register a new ACME account with email {email} without EAB") diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index febce5e81..98a89a731 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -35,15 +35,14 @@ export enum AcmeErrorType { export interface IAcmeError { type: AcmeErrorType; - detail: string; + message: string; status: number; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; } export class AcmeError extends Error implements IAcmeError { type: AcmeErrorType; - - detail: string; + message: string; status: number; @@ -53,22 +52,20 @@ export class AcmeError extends Error implements IAcmeError { constructor({ type, - detail, + message, status, subproblems, - error, - message + error }: { type: AcmeErrorType; - detail: string; + message: string; status: number; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; error?: unknown; - message?: string; }) { - super(message || detail); + super(message); this.type = type; - this.detail = detail; + this.message = message; this.status = status; this.subproblems = subproblems; this.error = error; @@ -78,7 +75,7 @@ export class AcmeError extends Error implements IAcmeError { toAcmeResponse(): IAcmeError { return { type: this.type, - detail: this.detail, + message: this.message, status: this.status, subproblems: this.subproblems }; @@ -90,20 +87,17 @@ export class AcmeError extends Error implements IAcmeError { */ export class AcmeMalformedError extends AcmeError { constructor({ - detail = "The request message was malformed", - error, - message + message = "The request message was malformed", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.Malformed, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeMalformedError"; } @@ -114,20 +108,17 @@ export class AcmeMalformedError extends AcmeError { */ export class AcmeUnauthorizedError extends AcmeError { constructor({ - detail = "The client lacks sufficient authorization", - error, - message + message = "The client lacks sufficient authorization", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.Unauthorized, - detail, + message, status: 403, - error, - message + error }); this.name = "AcmeUnauthorizedError"; } @@ -139,20 +130,17 @@ export class AcmeUnauthorizedError extends AcmeError { */ export class AcmeAccountDoesNotExistError extends AcmeError { constructor({ - detail = "The request specified an account that does not exist", - error, - message + message = "The request specified an account that does not exist", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.AccountDoesNotExist, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeAccountDoesNotExistError"; } @@ -163,20 +151,17 @@ export class AcmeAccountDoesNotExistError extends AcmeError { */ export class AcmeBadNonceError extends AcmeError { constructor({ - detail = "The client sent an unacceptable anti-replay nonce", - error, - message + message = "The client sent an unacceptable anti-replay nonce", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadNonce, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeBadNonceError"; } @@ -187,20 +172,17 @@ export class AcmeBadNonceError extends AcmeError { */ export class AcmeBadSignatureAlgorithmError extends AcmeError { constructor({ - detail = "The signature algorithm is invalid", - error, - message + message = "The signature algorithm is invalid", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadSignatureAlgorithm, - detail, + message, status: 401, - error, - message + error }); this.name = "AcmeBadSignatureAlgorithmError"; } @@ -211,20 +193,17 @@ export class AcmeBadSignatureAlgorithmError extends AcmeError { */ export class AcmeBadPublicKeyError extends AcmeError { constructor({ - detail = "The public key is not acceptable", - error, - message + message = "The public key is not acceptable", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadPublicKey, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeBadPublicKeyError"; } @@ -235,20 +214,17 @@ export class AcmeBadPublicKeyError extends AcmeError { */ export class AcmeBadCsrError extends AcmeError { constructor({ - detail = "The CSR is unacceptable", - error, - message + message = "The CSR is unacceptable", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadCsr, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeBadCsrError"; } @@ -260,20 +236,17 @@ export class AcmeBadCsrError extends AcmeError { */ export class AcmeBadRevocationReasonError extends AcmeError { constructor({ - detail = "The revocation reason provided is not allowed", - error, - message + message = "The revocation reason provided is not allowed", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadRevocationReason, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeBadRevocationReasonError"; } @@ -284,20 +257,17 @@ export class AcmeBadRevocationReasonError extends AcmeError { */ export class AcmeRateLimitedError extends AcmeError { constructor({ - detail = "The client has exceeded a rate limit", - error, - message + message = "The client has exceeded a rate limit", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.RateLimited, - detail, + message, status: 429, - error, - message + error }); this.name = "AcmeRateLimitedError"; } @@ -309,23 +279,20 @@ export class AcmeRateLimitedError extends AcmeError { */ export class AcmeRejectedIdentifierError extends AcmeError { constructor({ - detail = "The server will not issue certificates for the identifier", + message = "The server will not issue certificates for the identifier", subproblems, - error, - message + error }: { - detail?: string; + message?: string; subproblems?: Array<{ type: string; detail: string; identifier?: { type: string; value: string } }>; error?: unknown; - message?: string; } = {}) { super({ type: AcmeErrorType.RejectedIdentifier, - detail, + message, status: 400, subproblems, - error, - message + error }); this.name = "AcmeRejectedIdentifierError"; } @@ -336,20 +303,17 @@ export class AcmeRejectedIdentifierError extends AcmeError { */ export class AcmeServerInternalError extends AcmeError { constructor({ - detail = "An internal error occurred", - error, - message + message = "An internal error occurred", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.ServerInternal, - detail, + message, status: 500, - error, - message + error }); this.name = "AcmeServerInternalError"; } @@ -360,20 +324,17 @@ export class AcmeServerInternalError extends AcmeError { */ export class AcmeUnsupportedContactError extends AcmeError { constructor({ - detail = "A contact URL is of an unsupported type", - error, - message + message = "A contact URL is of an unsupported type", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.UnsupportedContact, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeUnsupportedContactError"; } @@ -385,20 +346,17 @@ export class AcmeUnsupportedContactError extends AcmeError { */ export class AcmeUnsupportedIdentifierError extends AcmeError { constructor({ - detail = "An identifier is of an unsupported type", - error, - message + message = "An identifier is of an unsupported type", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.UnsupportedIdentifier, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeUnsupportedIdentifierError"; } @@ -412,22 +370,19 @@ export class AcmeUserActionRequiredError extends AcmeError { instance?: string; constructor({ - detail = "Visit the instance URL and take actions specified there", + message = "Visit the instance URL and take actions specified there", instance, - error, - message + error }: { - detail?: string; + message?: string; instance?: string; error?: unknown; - message?: string; } = {}) { super({ type: AcmeErrorType.UserActionRequired, - detail, + message, status: 403, - error, - message + error }); this.instance = instance; this.name = "AcmeUserActionRequiredError"; @@ -446,20 +401,17 @@ export class AcmeUserActionRequiredError extends AcmeError { */ export class AcmeIncorrectResponseError extends AcmeError { constructor({ - detail = "The response is incorrect", - error, - message + message = "The response is incorrect", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.IncorrectResponse, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeIncorrectResponseError"; } @@ -470,20 +422,17 @@ export class AcmeIncorrectResponseError extends AcmeError { */ export class AcmeConnectionError extends AcmeError { constructor({ - detail = "A connection error occurred", - error, - message + message = "A connection error occurred", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.Connection, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeConnectionError"; } @@ -491,20 +440,17 @@ export class AcmeConnectionError extends AcmeError { export class AcmeDnsFailureError extends AcmeError { constructor({ - detail = "Hostname could not be resolved (DNS failure)", - error, - message + message = "Hostname could not be resolved (DNS failure)", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.DNS, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeDnsFailureError"; } @@ -512,20 +458,17 @@ export class AcmeDnsFailureError extends AcmeError { export class AcmeOrderNotReadyError extends AcmeError { constructor({ - detail = "The order is not ready", - error, - message + message = "The order is not ready", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.OrderNotReady, - detail, + message, status: 403, - error, - message + error }); this.name = "AcmeOrderNotReadyError"; } @@ -533,20 +476,17 @@ export class AcmeOrderNotReadyError extends AcmeError { export class AcmeBadCSRError extends AcmeError { constructor({ - detail = "The CSR is unacceptable", - error, - message + message = "The CSR is unacceptable", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.BadCsr, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeBadCSRError"; } @@ -554,20 +494,17 @@ export class AcmeBadCSRError extends AcmeError { export class AcmeExternalAccountRequiredError extends AcmeError { constructor({ - detail = "External account binding is required", - error, - message + message = "External account binding is required", + error }: { - detail?: string; - error?: unknown; message?: string; + error?: unknown; } = {}) { super({ type: AcmeErrorType.ExternalAccountRequired, - detail, + message, status: 400, - error, - message + error }); this.name = "AcmeExternalAccountRequiredError"; } diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index 828e0801b..08659c853 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -13,7 +13,7 @@ export const buildUrl = (profileId: string, path: string): string => { export const extractAccountIdFromKid = (kid: string, profileId: string): string => { const kidPrefix = buildUrl(profileId, "/accounts/"); if (!kid.startsWith(kidPrefix)) { - throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" }); + throw new AcmeMalformedError({ message: "KID must start with the profile account URL" }); } return z.string().uuid().parse(kid.slice(kidPrefix.length)); }; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 09faaf8bc..d928990a6 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -148,7 +148,7 @@ export const pkiAcmeServiceFactory = ({ try { result = await flattenedVerify(rawJwsPayload, async (protectedHeader: JWSHeaderParameters | undefined) => { if (protectedHeader === undefined) { - throw new AcmeMalformedError({ detail: "Protected header is required" }); + throw new AcmeMalformedError({ message: "Protected header is required" }); } const jwk = await getJWK(protectedHeader); const key = await importJWK(jwk, protectedHeader.alg); @@ -159,28 +159,28 @@ export const pkiAcmeServiceFactory = ({ throw error; } if (error instanceof ZodError) { - throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); + throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` }); } if (error instanceof errors.JWSSignatureVerificationFailed) { - throw new AcmeBadPublicKeyError({ detail: "Invalid JWS payload" }); + throw new AcmeBadPublicKeyError({ message: "Invalid JWS payload" }); } logger.error(error, "Unexpected error while verifying JWS payload"); - throw new AcmeServerInternalError({ detail: "Failed to verify JWS payload" }); + throw new AcmeServerInternalError({ message: "Failed to verify JWS payload" }); } const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); // Validate the URL if (new URL(protectedHeader.url).href !== url.href) { - throw new AcmeUnauthorizedError({ detail: "URL mismatch in the protected header" }); + throw new AcmeUnauthorizedError({ message: "URL mismatch in the protected header" }); } // Consume the nonce if (!protectedHeader.nonce) { - throw new AcmeMalformedError({ detail: "Nonce is required in the protected header" }); + throw new AcmeMalformedError({ message: "Nonce is required in the protected header" }); } const deleted = await keyStore.deleteItem(KeyStorePrefixes.PkiAcmeNonce(protectedHeader.nonce)); if (deleted !== 1) { - throw new AcmeBadNonceError({ detail: "Invalid nonce" }); + throw new AcmeBadNonceError({ message: "Invalid nonce" }); } // Parse the payload @@ -196,10 +196,10 @@ export const pkiAcmeServiceFactory = ({ throw error; } if (error instanceof ZodError) { - throw new AcmeMalformedError({ detail: `Invalid JWS payload: ${error.message}` }); + throw new AcmeMalformedError({ message: `Invalid JWS payload: ${error.message}` }); } logger.error(error, "Unexpected error while parsing JWS payload"); - throw new AcmeMalformedError({ detail: "Failed to verify JWS payload" }); + throw new AcmeMalformedError({ message: "Failed to verify JWS payload" }); } }; @@ -215,7 +215,7 @@ export const pkiAcmeServiceFactory = ({ rawJwsPayload, getJWK: async (protectedHeader) => { if (!protectedHeader.jwk) { - throw new AcmeMalformedError({ detail: "JWK is required in the protected header" }); + throw new AcmeMalformedError({ message: "JWK is required in the protected header" }); } return protectedHeader.jwk as unknown as JsonWebKey; }, @@ -246,7 +246,7 @@ export const pkiAcmeServiceFactory = ({ rawJwsPayload, getJWK: async (protectedHeader) => { if (!protectedHeader.kid) { - throw new AcmeMalformedError({ detail: "KID is required in the protected header" }); + throw new AcmeMalformedError({ message: "KID is required in the protected header" }); } const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); if (expectedAccountId && accountId !== expectedAccountId) { @@ -257,7 +257,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" }); } if (account.alg !== protectedHeader.alg) { - throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" }); + throw new AcmeMalformedError({ message: "ACME account algorithm mismatch" }); } return account.publicKey as JsonWebKey; }, @@ -344,7 +344,7 @@ export const pkiAcmeServiceFactory = ({ }): Promise> => { const profile = await validateAcmeProfile(profileId); if (!externalAccountBinding) { - throw new AcmeExternalAccountRequiredError({ detail: "External account binding is required" }); + throw new AcmeExternalAccountRequiredError({ message: "External account binding is required" }); } const publicKeyThumbprint = await calculateJwkThumbprint(jwk, "sha256"); @@ -363,26 +363,28 @@ export const pkiAcmeServiceFactory = ({ return { eabPayload: result.payload, eabProtectedHeader: result.protectedHeader }; } catch (error) { if (error instanceof errors.JWSSignatureVerificationFailed) { - throw new AcmeMalformedError({ detail: "Invalid external account binding JWS signature" }); + throw new AcmeExternalAccountRequiredError({ message: "Invalid external account binding JWS signature" }); } logger.error(error, "Unexpected error while verifying EAB JWS signature"); - throw new AcmeServerInternalError({ detail: "Failed to verify EAB JWS signature" }); + throw new AcmeServerInternalError({ message: "Failed to verify EAB JWS signature" }); } })(); const { alg: eabAlg, kid: eabKid } = eabProtectedHeader!; if (!["HS256", "HS384", "HS512"].includes(eabAlg!)) { - throw new AcmeMalformedError({ detail: "Invalid algorithm for external account binding JWS payload" }); + throw new AcmeExternalAccountRequiredError({ + message: "Invalid algorithm for external account binding JWS payload" + }); } // Make sure the KID in the EAB payload matches the profile ID if (eabKid !== profile.id) { - throw new UnauthorizedError({ message: "External account binding KID mismatch" }); + throw new AcmeExternalAccountRequiredError({ message: "External account binding KID mismatch" }); } // Make sure the URL matches the expected URL const url = eabProtectedHeader!.url!; if (url !== buildUrl(profile.id, "/new-account")) { - throw new UnauthorizedError({ message: "External account binding URL mismatch" }); + throw new AcmeExternalAccountRequiredError({ message: "External account binding URL mismatch" }); } // Make sure the JWK in the EAB payload matches the one provided in the outer JWS payload @@ -497,10 +499,10 @@ export const pkiAcmeServiceFactory = ({ const authorizations: TPkiAcmeAuths[] = await Promise.all( payload.identifiers.map(async (identifier) => { if (identifier.type !== AcmeIdentifierType.DNS) { - throw new AcmeUnsupportedIdentifierError({ detail: "Only DNS identifiers are supported" }); + throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" }); } if (isPrivateIp(identifier.value)) { - throw new AcmeUnsupportedIdentifierError({ detail: "Private IP addresses are not allowed" }); + throw new AcmeUnsupportedIdentifierError({ message: "Private IP addresses are not allowed" }); } const auth = await acmeAuthDAL.create( { @@ -647,9 +649,9 @@ export const pkiAcmeServiceFactory = ({ logger.error(exp, "Failed to sign certificate"); // TODO: audit log the error if (exp instanceof BadRequestError) { - errorToReturn = new AcmeBadCSRError({ detail: `Invalid CSR: ${exp.message}` }); + errorToReturn = new AcmeBadCSRError({ message: `Invalid CSR: ${exp.message}` }); } else { - errorToReturn = new AcmeServerInternalError({ detail: "Failed to sign certificate with internal error" }); + errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" }); } } return { diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index 4b29f6930..e703df5ef 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -252,8 +252,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider error: error.name, status: error.status, type: `urn:ietf:params:acme:error:${error.type}`, - detail: error.detail, - message: error.message + detail: error.message // TODO: add subproblems if they exist }); } else { From 330eb446da98f69004cffad74125856101fb8edd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:06:36 -0800 Subject: [PATCH 14/80] More test cases --- backend/bdd/features/pki/acme/account.feature | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 5685167b1..051c0459e 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -20,9 +20,10 @@ Feature: Account Then the value error with jq ".detail" should be equal to "" Examples: Bad Credentials - | eab_kid | eab_secret | error_type | error_msg | - | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | - | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | - | {acme_profile.eab_kid} | Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | - | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | urn:ietf:params:acme:error:externalAccountRequired | fixme | - | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | fixme | + | eab_kid | eab_secret | error_type | error_msg | + | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | + | {acme_profile.eab_kid} | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | + | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | + | bad | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch | + | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch | + | {acme_profile.eab_kid} | ABC{acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | From 8ee700bd39521a8249c33c38d92032b3c08c07b4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:22:08 -0800 Subject: [PATCH 15/80] More test cases --- backend/bdd/features/pki/acme/account.feature | 17 ++++++++++++++++- backend/bdd/features/steps/pki_acme.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 051c0459e..c10d93d9d 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -24,6 +24,21 @@ Feature: Account | bad | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | | {acme_profile.eab_kid} | Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | | {acme_profile.eab_kid} | YmFkLXNjcmV0Cg== | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | + | {acme_profile.eab_kid} | ABC{acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | | bad | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch | | 4bc7959c-fe2d-4447-ae91-0cd893667af6 | {acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | External account binding KID mismatch | - | {acme_profile.eab_kid} | ABC{acme_profile.eab_secret} | urn:ietf:params:acme:error:externalAccountRequired | Invalid external account binding JWS signature | + + Scenario Outline: Scenario: Create a new account with bad EAB url + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I use a different new-account URL "" for EAB signature + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account + Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" + Then the value error with jq ".detail" should be equal to "External account binding URL mismatch" + + Examples: Bad URLs + | url | + | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | + | {BASE_URL}/acme/new-account | + | https://example.com/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | + | bad | diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 0a753a827..c4da1f91a 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -232,17 +232,28 @@ def step_impl(context: Context): assert payload == replaced, f"{payload} != {replaced}" +@when('I use a different new-account URL "{url}" for EAB signature') +def step_impl(context: Context, url: str): + context.alt_eab_url = replace_vars(url, context.vars) + + @then( 'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' ) def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): acme_client = context.acme_client account_public_key = acme_client.net.key.public_key() + if hasattr(context, "alt_eab_url"): + eab_directory = messages.Directory.from_json( + {"newAccount": context.alt_eab_url} + ) + else: + eab_directory = acme_client.directory eab = messages.ExternalAccountBinding.from_data( account_public_key=account_public_key, kid=replace_vars(kid, context.vars), hmac_key=replace_vars(secret, context.vars), - directory=acme_client.directory, + directory=eab_directory, hmac_alg="HS256", ) registration = messages.NewRegistration.from_data( From 173eb48763c072c0c118e74268f60a0daeb06419 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:27:38 -0800 Subject: [PATCH 16/80] Add more corner cases --- backend/bdd/features/pki/acme/account.feature | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index c10d93d9d..4df675669 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -32,13 +32,15 @@ Feature: Account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory When I use a different new-account URL "" for EAB signature - Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" Then the value error with jq ".detail" should be equal to "External account binding URL mismatch" Examples: Bad URLs | url | | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | + | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account?foo=bar | + | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account#foobar | | {BASE_URL}/acme/new-account | | https://example.com/api/v1/pki/acme/profiles/{acme_profile.id}/new-account-bad | | bad | From 26bb0e51d8fcdde7399e46970f86b0f395b01ec4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:36:45 -0800 Subject: [PATCH 17/80] More test cases --- backend/bdd/features/pki/acme/account.feature | 8 ++++ backend/bdd/features/steps/pki_acme.py | 38 ++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 4df675669..e3673aee7 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -6,6 +6,14 @@ Feature: Account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) + Scenario: Find existing account + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I memorize acme_account.uri as account_uri + Then I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then the value acme_account.uri should be equal to "{account_uri}" + Scenario: Create a new account without EAB Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index c4da1f91a..f6e8a2636 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -237,10 +237,14 @@ def step_impl(context: Context, url: str): context.alt_eab_url = replace_vars(url, context.vars) -@then( - 'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' -) -def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): +def register_account_with_eab( + context: Context, + email: str, + kid: str, + secret: str, + account_var: str, + only_return_existing: bool = False, +): acme_client = context.acme_client account_public_key = acme_client.net.key.public_key() if hasattr(context, "alt_eab_url"): @@ -259,6 +263,7 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: registration = messages.NewRegistration.from_data( email=email, external_account_binding=eab, + only_return_existing=only_return_existing, ) try: context.vars[account_var] = acme_client.new_account(registration) @@ -266,6 +271,29 @@ def step_impl(context: Context, email: str, kid: str, secret: str, account_var: context.vars["error"] = exp +@then( + 'I register a new ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' +) +def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): + register_account_with_eab( + context=context, email=email, kid=kid, secret=secret, account_var=account_var + ) + + +@then( + 'I find the existing ACME account with email {email} and EAB key id "{kid}" with secret "{secret}" as {account_var}' +) +def step_impl(context: Context, email: str, kid: str, secret: str, account_var: str): + register_account_with_eab( + context=context, + email=email, + kid=kid, + secret=secret, + account_var=account_var, + only_return_existing=True, + ) + + @then("I register a new ACME account with email {email} without EAB") def step_impl(context: Context, email: str): acme_client = context.acme_client @@ -451,7 +479,7 @@ def step_impl(context: Context, var_path: str): @then("the value {var_path} should be equal to {expected}") def step_impl(context: Context, var_path: str, expected: str): value = eval_var(context, var_path) - expected_value = json.loads(expected) + expected_value = replace_vars(json.loads(expected), context.vars) assert value == expected_value, f"{value!r} does not match {expected_value!r}" From 795d0b36ec310927f5af88248f943c361d40d34e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:57:30 -0800 Subject: [PATCH 18/80] Add more test cases --- backend/bdd/features/pki/acme/account.feature | 2 +- backend/bdd/features/pki/acme/order.feature | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index e3673aee7..0920065ee 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -6,7 +6,7 @@ Feature: Account Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) - Scenario: Find existing account + Scenario: Find an existing account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 046dcda55..e923b17e7 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -72,3 +72,69 @@ Feature: Order Then the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] Then the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize Then the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true + + Scenario Outline: Create an order with invalid identifier types + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I peak and memorize the next nonce as nonce + When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "kid": "{acme_account.uri}" + }, + "payload": { + "identifiers": [ + { "type": "", "value": "www.example.org" } + ] + } + } + """ + + Examples: Bad Identifier Types + | identifier_type | + | bad | + | ip | + | email | + + Then the value response.status_code should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" + Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".detail" should be equal to "Only DNS identifiers are supported" + + Scenario Outline: Create an order with invalid identifier values + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I peak and memorize the next nonce as nonce + When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "kid": "{acme_account.uri}" + }, + "payload": { + "identifiers": [ + { "type": "dns", "value": "" } + ] + } + } + """ + + Examples: Bad Identifier Vluaes + | identifier_value | + | 127.0.0.1 | + | 192.168.123.111 | + | ../../etc/passwd | + + Then the value response.status_code should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" + Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".detail" should be equal to "Invalid DNS identifier" From 460dea84bebf71cb7a521a6d61de601ececfc74d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 19:58:19 -0800 Subject: [PATCH 19/80] More test cases # Conflicts: # backend/src/ee/services/pki-acme/pki-acme-schemas.ts # Conflicts: # backend/src/ee/services/pki-acme/pki-acme-service.ts --- backend/bdd/features/pki/acme/order.feature | 5 +++-- .../ee/services/pki-acme/pki-acme-schemas.ts | 2 ++ .../ee/services/pki-acme/pki-acme-service.ts | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index e923b17e7..aed647e47 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -130,8 +130,9 @@ Feature: Order Examples: Bad Identifier Vluaes | identifier_value | - | 127.0.0.1 | - | 192.168.123.111 | + | 127.0.0.1 | + | 192.168.123.111 | + | 169.254.169.254 | | ../../etc/passwd | Then the value response.status_code should be equal to 400 diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index 4c7d6c3c1..baa686e59 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -84,6 +84,8 @@ export const CreateAcmeAccountResponseSchema = z.object({ orders: z.string().optional() }); +export const ValidDNSIdentifierRegex = /^(?!-)[A-Za-z0-9-]{1,63}(? identifier.type !== AcmeIdentifierType.DNS)) { + throw new AcmeUnsupportedIdentifierError({ message: "Only DNS identifiers are supported" }); + } + if ( + payload.identifiers.some( + (identifier) => + !ValidDNSIdentifierRegex.test(identifier.value) || + isPrivateIp(identifier.value) || + (!getConfig().isDevelopmentMode && identifier.value.toLowerCase() === "localhost") + ) + ) { + throw new AcmeUnsupportedIdentifierError({ message: "Invalid DNS identifier" }); + } + const order = await acmeOrderDAL.transaction(async (tx) => { const account = (await acmeAccountDAL.findByProjectIdAndAccountId(profileId, accountId))!; const createdOrder = await acmeOrderDAL.create( From 37fc100ff7821cf575110a2c28af69818d476ab1 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:12:47 -0800 Subject: [PATCH 20/80] Add access control tests --- .../features/pki/acme/access-control.feature | 45 +++++++++++++++++++ backend/bdd/features/pki/acme/nonce.feature | 16 +++---- backend/bdd/features/pki/acme/order.feature | 4 +- backend/bdd/features/steps/pki_acme.py | 7 +++ 4 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 backend/bdd/features/pki/acme/access-control.feature diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature new file mode 100644 index 000000000..24d2a379c --- /dev/null +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -0,0 +1,45 @@ +Feature: Access Control + + Scenario Outline: Access across resources for a different account + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 + Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I put away current ACME client as client0 + + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account1.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 404 + + Examples: Endpoints + | src_var | jq | dest_var | url + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index d92770d2e..673191e16 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -22,25 +22,25 @@ Feature: Nonce Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I memorize with jq "" as - When I send a raw ACME request to "" + When I send a raw ACME request to "" """ { "protected": { "alg": "RS256", "nonce": "oFvnlFP1wIhRlYS2jTaXbA", - "url": "", + "url": "", "kid": "{acme_account.uri}" }, "payload": {} } """ Then the value response.status_code should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | path | + | src_var | jq | dest_var | url | | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | | order | . | not_used | {order.uri} | @@ -79,25 +79,25 @@ Feature: Nonce """ Then the value response.status_code should be equal to 200 Then I memorize with jq "" as - When I send a raw ACME request to "" + When I send a raw ACME request to "" """ { "protected": { "alg": "RS256", "nonce": "{nonce_value}", - "url": "", + "url": "", "kid": "{acme_account.uri}" }, "payload": {} } """ Then the value response.status_code should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" Then the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints - | src_var | jq | dest_var | path | + | src_var | jq | dest_var | url | | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order | | order | . | not_used | {order.uri} | diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index aed647e47..a4cf4f121 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -102,8 +102,8 @@ Feature: Order | email | Then the value response.status_code should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" Then the value response with jq ".detail" should be equal to "Only DNS identifiers are supported" Scenario Outline: Create an order with invalid identifier values @@ -136,6 +136,6 @@ Feature: Order | ../../etc/passwd | Then the value response.status_code should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" Then the value response with jq ".detail" should be equal to "Invalid DNS identifier" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index f6e8a2636..52a234644 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -499,6 +499,13 @@ def step_impl(context: Context, var_name: str): context.vars[var_name] = json_util.encode_b64jose(list(acme_client.net._nonces)[0]) +@then("I put away current ACME client as {var_name}") +def step_impl(context: Context, var_name: str): + acme_client = context.acme_client + del context.acme_client + context.vars[var_name] = acme_client + + @then("I memorize {var_path} as {var_name}") def step_impl(context: Context, var_path: str, var_name: str): value = eval_var(context, var_path) From ebc041ad9df5ce6cd3f3a5ad3174c860d8cb3d41 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:28:55 -0800 Subject: [PATCH 21/80] Check url parsing error --- backend/bdd/features/pki/acme/auth.feature | 1 - .../bdd/features/pki/acme/dicrectory.feature | 14 ++-- backend/bdd/features/pki/acme/nonce.feature | 66 +++++++++---------- backend/bdd/features/pki/acme/order.feature | 28 ++++---- .../ee/services/pki-acme/pki-acme-service.ts | 12 +++- 5 files changed, 63 insertions(+), 58 deletions(-) diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 4605e2eef..6de64dc30 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -3,7 +3,6 @@ Feature: Authorization Scenario: Get authorization Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr diff --git a/backend/bdd/features/pki/acme/dicrectory.feature b/backend/bdd/features/pki/acme/dicrectory.feature index dbb980ac3..1ba43871c 100644 --- a/backend/bdd/features/pki/acme/dicrectory.feature +++ b/backend/bdd/features/pki/acme/dicrectory.feature @@ -5,10 +5,10 @@ Feature: Directory When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then the response status code should be "200" Then the response body should match JSON value - """ - { - "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", - "newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account", - "newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" - } - """ + """ + { + "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", + "newAccount": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-account", + "newOrder": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" + } + """ diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 673191e16..0116a89ed 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -23,17 +23,17 @@ Feature: Nonce Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I memorize with jq "" as When I send a raw ACME request to "" - """ - { - "protected": { - "alg": "RS256", - "nonce": "oFvnlFP1wIhRlYS2jTaXbA", - "url": "", - "kid": "{acme_account.uri}" - }, - "payload": {} - } - """ + """ + { + "protected": { + "alg": "RS256", + "nonce": "oFvnlFP1wIhRlYS2jTaXbA", + "url": "", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ Then the value response.status_code should be equal to 400 Then the value response with jq ".status" should be equal to 400 Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" @@ -66,31 +66,31 @@ Feature: Nonce Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order Then I peak and memorize the next nonce as nonce_value When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce_value}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", - "kid": "{acme_account.uri}" - }, - "payload": {} - } - """ + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce_value}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ Then the value response.status_code should be equal to 200 Then I memorize with jq "" as When I send a raw ACME request to "" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce_value}", - "url": "", - "kid": "{acme_account.uri}" - }, - "payload": {} - } - """ + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce_value}", + "url": "", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ Then the value response.status_code should be equal to 400 Then the value response with jq ".status" should be equal to 400 Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index a4cf4f121..e2ea64e91 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -112,21 +112,21 @@ Feature: Order Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I peak and memorize the next nonce as nonce When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce}", - "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", - "kid": "{acme_account.uri}" - }, - "payload": { - "identifiers": [ - { "type": "dns", "value": "" } - ] + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order", + "kid": "{acme_account.uri}" + }, + "payload": { + "identifiers": [ + { "type": "dns", "value": "" } + ] + } } - } - """ + """ Examples: Bad Identifier Vluaes | identifier_value | diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 3f634d8e8..6e45f2a92 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -41,7 +41,6 @@ import { AcmeMalformedError, AcmeOrderNotReadyError, AcmeServerInternalError, - AcmeUnauthorizedError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; import { buildUrl, extractAccountIdFromKid } from "./pki-acme-fns"; @@ -171,9 +170,16 @@ export const pkiAcmeServiceFactory = ({ const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { const protectedHeader = ProtectedHeaderSchema.parse(rawProtectedHeader); + const parsedUrl = (() => { + try { + return new URL(protectedHeader.url); + } catch (error) { + throw new AcmeMalformedError({ message: "Invalid URL in the protected header" }); + } + })(); // Validate the URL - if (new URL(protectedHeader.url).href !== url.href) { - throw new AcmeUnauthorizedError({ message: "URL mismatch in the protected header" }); + if (parsedUrl.href !== url.href) { + throw new AcmeMalformedError({ message: "URL mismatch in the protected header" }); } // Consume the nonce if (!protectedHeader.nonce) { From 1cfbfb80fd7d003d10fbcaad888656b723a4ddd7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:39:26 -0800 Subject: [PATCH 22/80] More test cases --- .../features/pki/acme/access-control.feature | 104 +++++++++++++----- 1 file changed, 77 insertions(+), 27 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 24d2a379c..2fd346b5b 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -1,10 +1,54 @@ Feature: Access Control - - Scenario Outline: Access across resources for a different account +# +# Scenario Outline: Access across resources for a different account +# Given I have an ACME cert profile as "acme_profile" +# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 +# Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id +# When I create certificate signing request as csr +# Then I add names to certificate signing request csr +# """ +# { +# "COMMON_NAME": "localhost" +# } +# """ +# Then I create a RSA private key pair as cert_key +# Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format +# Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order +# And I put away current ACME client as client0 +# +# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory +# Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 +# Then I peak and memorize the next nonce as nonce +# Then I memorize with jq "" as +# When I send a raw ACME request to "" +# """ +# { +# "protected": { +# "alg": "RS256", +# "nonce": "{nonce}", +# "url": "", +# "kid": "{acme_account1.uri}" +# }, +# "payload": {} +# } +# """ +# Then the value response.status_code should be equal to 404 +# +# Examples: Endpoints +# | src_var | jq | dest_var | url +# | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | +# | order | . | not_used | {order.uri} | +# | order | . | not_used | {order.uri}/finalize | +# | order | . | not_used | {order.uri}/certificate | +# | order | .authorizations[0].uri | auth_uri | {auth_uri} | +# | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | +# + Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 - Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -15,31 +59,37 @@ Feature: Access Control Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - And I put away current ACME client as client0 - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce Then I memorize with jq "" as - When I send a raw ACME request to "" - """ - { - "protected": { - "alg": "RS256", - "nonce": "{nonce}", - "url": "", - "kid": "{acme_account1.uri}" - }, - "payload": {} - } - """ - Then the value response.status_code should be equal to 404 + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 400 + Then the value response with jq ".status" should be equal to 400 + Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:malformed" + Then the value response with jq ".detail" should be equal to "" Examples: Endpoints - | src_var | jq | dest_var | url - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | . | not_used | {order.uri}/certificate | - | order | .authorizations[0].uri | auth_uri | {auth_uri} | - | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | + | src_var | jq | dest_var | actual_url | bad_url | error_detail | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | BAD | Invalid URL in the protected header | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | https://evil.com/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders | URL mismatch in the protected header | + | order | . | not_used | {order.uri} | BAD | Invalid URL in the protected header | + | order | . | not_used | {order.uri} | https://example.com/acmes/orders/FOOBAR | URL mismatch in the protected header | + | order | . | not_used | {order.uri}/finalize | BAD | Invalid URL in the protected header | + | order | . | not_used | {order.uri}/finalize | https://example.com/acmes/orders/FOOBAR/finalize | URL mismatch in the protected header | + | order | . | not_used | {order.uri}/certificate | BAD | Invalid URL in the protected header | + | order | . | not_used | {order.uri}/certificate | https://example.com/acmes/orders/FOOBAR/certificate | URL mismatch in the protected header | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | BAD | Invalid URL in the protected header | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | https://example.com/acmes/auths/FOOBAR | URL mismatch in the protected header | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | BAD | Invalid URL in the protected header | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | https://example.com/acmes/challenges/FOOBAR | URL mismatch in the protected header | From e370d16db27657c782486a8b94e7655339a88129 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:39:56 -0800 Subject: [PATCH 23/80] Uncomment --- .../features/pki/acme/access-control.feature | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 2fd346b5b..ed3f85ccf 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -1,49 +1,49 @@ Feature: Access Control -# -# Scenario Outline: Access across resources for a different account -# Given I have an ACME cert profile as "acme_profile" -# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 -# Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id -# When I create certificate signing request as csr -# Then I add names to certificate signing request csr -# """ -# { -# "COMMON_NAME": "localhost" -# } -# """ -# Then I create a RSA private key pair as cert_key -# Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format -# Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order -# And I put away current ACME client as client0 -# -# When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 -# Then I peak and memorize the next nonce as nonce -# Then I memorize with jq "" as -# When I send a raw ACME request to "" -# """ -# { -# "protected": { -# "alg": "RS256", -# "nonce": "{nonce}", -# "url": "", -# "kid": "{acme_account1.uri}" -# }, -# "payload": {} -# } -# """ -# Then the value response.status_code should be equal to 404 -# -# Examples: Endpoints -# | src_var | jq | dest_var | url -# | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | -# | order | . | not_used | {order.uri} | -# | order | . | not_used | {order.uri}/finalize | -# | order | . | not_used | {order.uri}/certificate | -# | order | .authorizations[0].uri | auth_uri | {auth_uri} | -# | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | -# + + Scenario Outline: Access across resources for a different account + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 + Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I put away current ACME client as client0 + + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account1.uri}" + }, + "payload": {} + } + """ + Then the value response.status_code should be equal to 404 + + Examples: Endpoints + | src_var | jq | dest_var | url + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | + | order | . | not_used | {order.uri} | + | order | . | not_used | {order.uri}/finalize | + | order | . | not_used | {order.uri}/certificate | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | + Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory From da8a44a36dc1cbedbfdf85a73f182e1634b3d2a8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:41:07 -0800 Subject: [PATCH 24/80] Remove TODO for now --- backend/bdd/features/pki/acme/order.feature | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index e2ea64e91..0a4ab4d17 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -3,7 +3,6 @@ Feature: Order Scenario: Create a new order Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -24,7 +23,6 @@ Feature: Order Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -55,7 +53,6 @@ Feature: Order Scenario: Fetch an order Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory -# # TODO: make it I have an account already instead? Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr From d7cf4388feb68f138d2b30ab6d3561c3a4e72176 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 20:41:56 -0800 Subject: [PATCH 25/80] More test cases --- backend/bdd/features/pki/acme/order.feature | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 0a4ab4d17..1fc1fb017 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -131,6 +131,9 @@ Feature: Order | 192.168.123.111 | | 169.254.169.254 | | ../../etc/passwd | + | !@#$ | + | ! | + | https://evil.com | Then the value response.status_code should be equal to 400 Then the value response with jq ".status" should be equal to 400 From 4150035fcdd9a45ac64f61362bef9de8d544ff1b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 22:33:21 -0800 Subject: [PATCH 26/80] More test cases --- .../features/pki/acme/access-control.feature | 40 +++++++++++++------ backend/bdd/features/steps/pki_acme.py | 25 ++++++++++-- .../ee/services/pki-acme/pki-acme-errors.ts | 2 +- .../ee/services/pki-acme/pki-acme-service.ts | 4 +- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index ed3f85ccf..81b48b025 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -15,12 +15,28 @@ Feature: Access Control Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - And I put away current ACME client as client0 - - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account0.uri}" + }, + "payload": {"invalid": "payload"} + } + """ + # With original owner account, the invalid payload is going to trigger other errors instead of 404, this is to make sure + # that our URLs are actually correct + Then the value response.status_code should not be equal to 404 + And I put away current ACME client as client0 + + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 + Then I peak and memorize the next nonce as nonce When I send a raw ACME request to "" """ { @@ -30,19 +46,19 @@ Feature: Access Control "url": "", "kid": "{acme_account1.uri}" }, - "payload": {} + "raw_payload": "" } """ Then the value response.status_code should be equal to 404 Examples: Endpoints - | src_var | jq | dest_var | url - | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | - | order | . | not_used | {order.uri} | - | order | . | not_used | {order.uri}/finalize | - | order | . | not_used | {order.uri}/certificate | - | order | .authorizations[0].uri | auth_uri | {auth_uri} | - | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | + | src_var | jq | dest_var | url | payload | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {order.uri} | | + | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | + | order | . | not_used | {order.uri}/certificate | | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 52a234644..88c31c4a0 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -310,11 +310,21 @@ def send_raw_acme_req(context: Context, url: str): acme_client = context.acme_client content = json.loads(context.text) protected = replace_vars(content["protected"], context.vars) - payload = ( - replace_vars(content["payload"], context.vars) if "payload" in content else None - ) alg = acme_client.net.alg - encoded_payload = json.dumps(payload).encode() if payload is not None else b"" + if "raw_payload" in content: + encoded_payload = content["raw_payload"].encode("utf-8") + else: + if "payload" not in content: + payload = ( + replace_vars(content["payload"], context.vars) + if "payload" in content + else None + ) + encoded_payload = ( + json.dumps(payload).encode() if payload is not None else b"" + ) + else: + encoded_payload = b"" protected_headers = json.dumps(protected) signature = alg.sign( key=acme_client.net.key.key, @@ -483,6 +493,13 @@ def step_impl(context: Context, var_path: str, expected: str): assert value == expected_value, f"{value!r} does not match {expected_value!r}" +@then("the value {var_path} should not be equal to {expected}") +def step_impl(context: Context, var_path: str, expected: str): + value = eval_var(context, var_path) + expected_value = replace_vars(json.loads(expected), context.vars) + assert value != expected_value, f"{value!r} does match {expected_value!r}" + + @then('I memorize {var_path} with jq "{jq_query}" as {var_name}') def step_impl(context: Context, var_path: str, jq_query, var_name: str): _, value = apply_value_with_jq( diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index 98a89a731..ba3c9fbc0 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -139,7 +139,7 @@ export class AcmeAccountDoesNotExistError extends AcmeError { super({ type: AcmeErrorType.AccountDoesNotExist, message, - status: 400, + status: 404, error }); this.name = "AcmeAccountDoesNotExistError"; diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 6e45f2a92..3190f98ef 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -165,7 +165,7 @@ export const pkiAcmeServiceFactory = ({ throw new AcmeBadPublicKeyError({ message: "Invalid JWS payload" }); } logger.error(error, "Unexpected error while verifying JWS payload"); - throw new AcmeServerInternalError({ message: "Failed to verify JWS payload" }); + throw new AcmeMalformedError({ message: "Failed to verify JWS payload" }); } const { protectedHeader: rawProtectedHeader, payload: rawPayload } = result; try { @@ -257,7 +257,7 @@ export const pkiAcmeServiceFactory = ({ } const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId); if (expectedAccountId && accountId !== expectedAccountId) { - throw new NotFoundError({ message: "ACME resource not found" }); + throw new AcmeAccountDoesNotExistError({ message: "ACME resource not found" }); } const account = await acmeAccountDAL.findByProjectIdAndAccountId(profile.id, accountId); if (!account) { From 4c5d7f3c9d6af22953383da54e7f0771dc96cf82 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 22:44:46 -0800 Subject: [PATCH 27/80] More tests --- .../features/pki/acme/access-control.feature | 68 ++++++++++++++++++- backend/bdd/features/pki/acme/account.feature | 20 +++--- backend/bdd/features/pki/acme/auth.feature | 14 ++-- .../features/pki/acme/cert-profile.feature | 18 ++--- .../bdd/features/pki/acme/challenge.feature | 16 ++--- .../bdd/features/pki/acme/dicrectory.feature | 2 +- backend/bdd/features/pki/acme/nonce.feature | 32 ++++----- backend/bdd/features/pki/acme/order.feature | 61 +++++++++-------- 8 files changed, 149 insertions(+), 82 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 81b48b025..f39d087a3 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -1,6 +1,6 @@ Feature: Access Control - Scenario Outline: Access across resources for a different account + Scenario Outline: Access across resources across different account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 @@ -60,6 +60,72 @@ Feature: Access Control | order | .authorizations[0].uri | auth_uri | {auth_uri} | | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | + Scenario Outline: Access resources across a different profile + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 + Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + # With original owner account under their profile, the invalid payload is going to trigger other errors instead of + # 404, this is to make sure that our URLs are actually correct + Then the value response.status_code should not be equal to 404 + And I put away current ACME client as client0 + + Given I make a random slug as profile_slug + Given I use AUTH_TOKEN for authentication + When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + """ + { + "projectId": "{PROJECT_ID}", + "slug": "{profile_slug}", + "description": "", + "enrollmentType": "acme", + "caId": "{CERT_CA_ID}", + "certificateTemplateId": "{CERT_TEMPLATE_ID}", + "acmeConfig": {} + } + """ + Then the value response.status_code should be equal to 200 + Then I memorize response with jq ".certificateProfile.id" as profile_id + When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + Then I memorize response with jq ".eabKid" as eab_kid + And I memorize response with jq ".eabSecret" as eab_secret + When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory + Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1 + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account1.uri}" + }, + "raw_payload": "" + } + """ + Then the value response.status_code should be equal to 404 + + Examples: Endpoints + | src_var | jq | dest_var | url | payload | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {order.uri} | | + | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | + | order | . | not_used | {order.uri}/certificate | | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | + Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 0920065ee..2c0ce3566 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -4,28 +4,28 @@ Feature: Account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) + And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) Scenario: Find an existing account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then I memorize acme_account.uri as account_uri - Then I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then the value acme_account.uri should be equal to "{account_uri}" + And I memorize acme_account.uri as account_uri + And I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account + And the value acme_account.uri should be equal to "{account_uri}" Scenario: Create a new account without EAB Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com without EAB - Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" + And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" Scenario Outline: Scenario: Create a new account with bad EAB credentials Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account - Then the value error with jq ".type" should be equal to "" - Then the value error with jq ".detail" should be equal to "" + And the value error with jq ".type" should be equal to "" + And the value error with jq ".detail" should be equal to "" Examples: Bad Credentials | eab_kid | eab_secret | error_type | error_msg | @@ -39,10 +39,10 @@ Feature: Account Scenario Outline: Scenario: Create a new account with bad EAB url Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory - When I use a different new-account URL "" for EAB signature + And I use a different new-account URL "" for EAB signature Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" - Then the value error with jq ".detail" should be equal to "External account binding URL mismatch" + And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" + And the value error with jq ".detail" should be equal to "External account binding URL mismatch" Examples: Bad URLs | url | diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 6de64dc30..65b8ec114 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -12,11 +12,11 @@ Feature: Authorization } """ Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) - Then the value order.authorizations[0].body with jq ".status" should be equal to "pending" - Then the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And the value order.authorizations[0].uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/(.+) + And the value order.authorizations[0].body with jq ".status" should be equal to "pending" + And the value order.authorizations[0].body with jq ".challenges | map(pick(.type, .status)) | sort_by(.type)" should be equal to json """ [ { @@ -25,8 +25,8 @@ Feature: Authorization } ] """ - Then the value order.authorizations[0].body with jq ".challenges | map(.status) | sort" should be equal to ["pending"] - Then the value order.authorizations[0].body with jq ".identifier" should be equal to json + And the value order.authorizations[0].body with jq ".challenges | map(.status) | sort" should be equal to ["pending"] + And the value order.authorizations[0].body with jq ".identifier" should be equal to json """ { "type": "dns", diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 2a325c7c1..92a921dde 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -2,7 +2,7 @@ Feature: ACME Cert Profile Scenario: Create a cert profile Given I make a random slug as profile_slug - Given I use AUTH_TOKEN for authentication + And I use AUTH_TOKEN for authentication When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload """ { @@ -16,15 +16,15 @@ Feature: ACME Cert Profile } """ Then the value response.status_code should be equal to 200 - Then the value response with jq ".certificateProfile.id" should be present - Then the value response with jq ".certificateProfile.slug" should be equal to "{profile_slug}" - Then the value response with jq ".certificateProfile.caId" should be equal to "{CERT_CA_ID}" - Then the value response with jq ".certificateProfile.certificateTemplateId" should be equal to "{CERT_TEMPLATE_ID}" - Then the value response with jq ".certificateProfile.enrollmentType" should be equal to "acme" + And the value response with jq ".certificateProfile.id" should be present + And the value response with jq ".certificateProfile.slug" should be equal to "{profile_slug}" + And the value response with jq ".certificateProfile.caId" should be equal to "{CERT_CA_ID}" + And the value response with jq ".certificateProfile.certificateTemplateId" should be equal to "{CERT_TEMPLATE_ID}" + And the value response with jq ".certificateProfile.enrollmentType" should be equal to "acme" Scenario: Reveal EAB secret Given I make a random slug as profile_slug - Given I use AUTH_TOKEN for authentication + And I use AUTH_TOKEN for authentication When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload """ { @@ -41,8 +41,8 @@ Feature: ACME Cert Profile And I memorize response with jq ".certificateProfile.id" as profile_id When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then the value response.status_code should be equal to 200 - Then the value response with jq ".eabKid" should be equal to "{profile_id}" - Then the value response with jq ".eabSecret" should be present + And the value response with jq ".eabKid" should be equal to "{profile_id}" + And the value response with jq ".eabSecret" should be present And I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index ece895848..b36a3e328 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -11,11 +11,11 @@ Feature: Challenge "COMMON_NAME": "localhost" } """ - Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I select challenge with type http-01 for domain localhost from order at order as challenge - Then I serve challenge response for challenge at localhost - Then I tell ACME server that challenge is ready to be verified - Then I poll and finalize the ACME order order as finalized_order - Then the value finalized_order.body with jq ".status" should be equal to "valid" + And I create a RSA private key pair as cert_key + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I select challenge with type http-01 for domain localhost from order at order as challenge + And I serve challenge response for challenge at localhost + And I tell ACME server that challenge is ready to be verified + And I poll and finalize the ACME order order as finalized_order + And the value finalized_order.body with jq ".status" should be equal to "valid" diff --git a/backend/bdd/features/pki/acme/dicrectory.feature b/backend/bdd/features/pki/acme/dicrectory.feature index 1ba43871c..664ff7457 100644 --- a/backend/bdd/features/pki/acme/dicrectory.feature +++ b/backend/bdd/features/pki/acme/dicrectory.feature @@ -4,7 +4,7 @@ Feature: Directory Given I have an ACME cert profile as "acme_profile" When I send a "GET" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then the response status code should be "200" - Then the response body should match JSON value + And the response body should match JSON value """ { "newNonce": "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce", diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 0116a89ed..601c53a85 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -4,13 +4,13 @@ Feature: Nonce Given I have an ACME cert profile as "acme_profile" When I send a "HEAD" request to "/api/v1/pki/acme/profiles/{acme_profile.id}/new-nonce" Then the response status code should be "200" - Then the response header "Replay-Nonce" should contains non-empty value + And the response header "Replay-Nonce" should contains non-empty value Scenario Outline: Send a bad nonce to account endpoints Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id + And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -19,9 +19,9 @@ Feature: Nonce } """ Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I memorize with jq "" as + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I memorize with jq "" as When I send a raw ACME request to "" """ { @@ -35,9 +35,9 @@ Feature: Nonce } """ Then the value response.status_code should be equal to 400 - Then the value response with jq ".status" should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" - Then the value response with jq ".detail" should be equal to "Invalid nonce" + And the value response with jq ".status" should be equal to 400 + And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" + And the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints | src_var | jq | dest_var | url | @@ -53,7 +53,7 @@ Feature: Nonce Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id + And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr Then I add names to certificate signing request csr """ @@ -62,9 +62,9 @@ Feature: Nonce } """ Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I peak and memorize the next nonce as nonce_value + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I peak and memorize the next nonce as nonce_value When I send a raw ACME request to "/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account_id}/orders" """ { @@ -78,7 +78,7 @@ Feature: Nonce } """ Then the value response.status_code should be equal to 200 - Then I memorize with jq "" as + And I memorize with jq "" as When I send a raw ACME request to "" """ { @@ -92,9 +92,9 @@ Feature: Nonce } """ Then the value response.status_code should be equal to 400 - Then the value response with jq ".status" should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" - Then the value response with jq ".detail" should be equal to "Invalid nonce" + And the value response with jq ".status" should be equal to 400 + And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badNonce" + And the value response with jq ".detail" should be equal to "Invalid nonce" Examples: Endpoints | src_var | jq | dest_var | url | diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 1fc1fb017..9e8b47980 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -12,13 +12,13 @@ Feature: Order } """ Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) - Then the value order.body with jq ".status" should be equal to "pending" - Then the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] - Then the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And the value order.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+) + And the value order.body with jq ".status" should be equal to "pending" + And the value order.body with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] + And the value order.body with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + And the value order.body with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" @@ -31,17 +31,17 @@ Feature: Order "COMMON_NAME": "localhost" } """ - Then I add subject alternative name to certificate signing request csr + And I add subject alternative name to certificate signing request csr """ [ "example.com", "infisical.com" ] """ - Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then the value order.body with jq ".identifiers | sort_by(.value)" should be equal to json + And I create a RSA private key pair as cert_key + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And the value order.body with jq ".identifiers | sort_by(.value)" should be equal to json """ [ {"type": "dns", "value": "example.com"}, @@ -62,19 +62,19 @@ Feature: Order } """ Then I create a RSA private key pair as cert_key - Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format - Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order - Then I send an ACME post-as-get to order.uri as fetched_order - Then the value fetched_order with jq ".status" should be equal to "pending" - Then the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] - Then the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize - Then the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true + And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + And I send an ACME post-as-get to order.uri as fetched_order + And the value fetched_order with jq ".status" should be equal to "pending" + And the value fetched_order with jq ".identifiers" should be equal to [{"type": "dns", "value": "localhost"}] + And the value fetched_order with jq ".finalize" should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/orders/(.+)/finalize + And the value fetched_order with jq "all(.authorizations[]; startswith("{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/authorizations/"))" should be equal to true Scenario Outline: Create an order with invalid identifier types Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then I peak and memorize the next nonce as nonce + And I peak and memorize the next nonce as nonce When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" """ { @@ -92,22 +92,22 @@ Feature: Order } """ + Then the value response.status_code should be equal to 400 + And the value response with jq ".status" should be equal to 400 + And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" + And the value response with jq ".detail" should be equal to "Only DNS identifiers are supported" + Examples: Bad Identifier Types | identifier_type | | bad | | ip | | email | - Then the value response.status_code should be equal to 400 - Then the value response with jq ".status" should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" - Then the value response with jq ".detail" should be equal to "Only DNS identifiers are supported" - Scenario Outline: Create an order with invalid identifier values Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account - Then I peak and memorize the next nonce as nonce + And I peak and memorize the next nonce as nonce When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" """ { @@ -125,6 +125,11 @@ Feature: Order } """ + Then the value response.status_code should be equal to 400 + And the value response with jq ".status" should be equal to 400 + And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" + And the value response with jq ".detail" should be equal to "Invalid DNS identifier" + Examples: Bad Identifier Vluaes | identifier_value | | 127.0.0.1 | @@ -135,7 +140,3 @@ Feature: Order | ! | | https://evil.com | - Then the value response.status_code should be equal to 400 - Then the value response with jq ".status" should be equal to 400 - Then the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" - Then the value response with jq ".detail" should be equal to "Invalid DNS identifier" From a0571002743f0549e68322f70eefeecae6ea978e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 22:54:52 -0800 Subject: [PATCH 28/80] More test cases --- .../features/pki/acme/access-control.feature | 91 ++++++++++++++++++- backend/bdd/features/pki/acme/account.feature | 10 +- backend/bdd/features/pki/acme/auth.feature | 2 +- .../features/pki/acme/cert-profile.feature | 2 +- .../bdd/features/pki/acme/challenge.feature | 2 +- backend/bdd/features/pki/acme/nonce.feature | 4 +- backend/bdd/features/pki/acme/order.feature | 12 +-- backend/bdd/features/steps/pki_acme.py | 33 ++++--- 8 files changed, 124 insertions(+), 32 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index f39d087a3..96da3f7c8 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -2,7 +2,7 @@ Feature: Access Control Scenario Outline: Access across resources across different account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id When I create certificate signing request as csr @@ -34,7 +34,7 @@ Feature: Access Control Then the value response.status_code should not be equal to 404 And I put away current ACME client as client0 - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email maidu@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce When I send a raw ACME request to "" @@ -62,7 +62,87 @@ Feature: Access Control Scenario Outline: Access resources across a different profile Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" + Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 + Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + Then I create a RSA private key pair as cert_key + Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format + Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account0.uri}" + }, + "payload": {"invalid": "payload"} + } + """ + # With original owner account under their profile, the invalid payload is going to trigger other errors instead of + # 404, this is to make sure that our URLs are actually correct + Then the value response.status_code should not be equal to 404 + And I put away current ACME client as client0 + + Given I make a random slug as profile_slug + Given I use AUTH_TOKEN for authentication + When I send a "POST" request to "/api/v1/pki/certificate-profiles" with JSON payload + """ + { + "projectId": "{PROJECT_ID}", + "slug": "{profile_slug}", + "description": "", + "enrollmentType": "acme", + "caId": "{CERT_CA_ID}", + "certificateTemplateId": "{CERT_TEMPLATE_ID}", + "acmeConfig": {} + } + """ + Then the value response.status_code should be equal to 200 + Then I memorize response with jq ".certificateProfile.id" as profile_id + When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" + Then I memorize response with jq ".eabKid" as eab_kid + And I memorize response with jq ".eabSecret" as eab_secret + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" + Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1 + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account1.uri}" + }, + "raw_payload": "" + } + """ + Then the value response.status_code should be equal to 404 + + Examples: Endpoints + | src_var | jq | dest_var | url | payload | + | order | . | not_used | {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/{account0_id}/orders | | + | order | . | not_used | {order.uri} | | + | order | . | not_used | {order.uri}/finalize | {\"csr\": \"\"} | + | order | . | not_used | {order.uri}/certificate | | + | order | .authorizations[0].uri | auth_uri | {auth_uri} | | + | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | + + Scenario Outline: Access resources across a different profile with the same key pair + Given I have an ACME cert profile as "acme_profile" + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 Then I memorize acme_account0.uri with jq "capture("/(?[^/]+)$") | .id" as account0_id When I create certificate signing request as csr @@ -99,7 +179,7 @@ Feature: Access Control When I send a "GET" request to "/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal" Then I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" with the key pair from client0 Then I register a new ACME account with email maidu@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account1 Then I peak and memorize the next nonce as nonce Then I memorize with jq "" as @@ -126,9 +206,10 @@ Feature: Access Control | order | .authorizations[0].uri | auth_uri | {auth_uri} | | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | + Scenario Outline: URL mismatch Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account Then I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr diff --git a/backend/bdd/features/pki/acme/account.feature b/backend/bdd/features/pki/acme/account.feature index 2c0ce3566..589c5ab24 100644 --- a/backend/bdd/features/pki/acme/account.feature +++ b/backend/bdd/features/pki/acme/account.feature @@ -2,13 +2,13 @@ Feature: Account Scenario: Create a new account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And the value acme_account.uri with jq "." should match pattern {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/accounts/(.+) Scenario: Find an existing account Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And I memorize acme_account.uri as account_uri And I find the existing ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account @@ -16,13 +16,13 @@ Feature: Account Scenario: Create a new account without EAB Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com without EAB And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" Scenario Outline: Scenario: Create a new account with bad EAB credentials Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "" with secret "" as acme_account And the value error with jq ".type" should be equal to "" And the value error with jq ".detail" should be equal to "" @@ -38,7 +38,7 @@ Feature: Account Scenario Outline: Scenario: Create a new account with bad EAB url Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" And I use a different new-account URL "" for EAB signature Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And the value error with jq ".type" should be equal to "urn:ietf:params:acme:error:externalAccountRequired" diff --git a/backend/bdd/features/pki/acme/auth.feature b/backend/bdd/features/pki/acme/auth.feature index 65b8ec114..46cc9d4e2 100644 --- a/backend/bdd/features/pki/acme/auth.feature +++ b/backend/bdd/features/pki/acme/auth.feature @@ -2,7 +2,7 @@ Feature: Authorization Scenario: Get authorization Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr diff --git a/backend/bdd/features/pki/acme/cert-profile.feature b/backend/bdd/features/pki/acme/cert-profile.feature index 92a921dde..3c292e8ba 100644 --- a/backend/bdd/features/pki/acme/cert-profile.feature +++ b/backend/bdd/features/pki/acme/cert-profile.feature @@ -45,5 +45,5 @@ Feature: ACME Cert Profile And the value response with jq ".eabSecret" should be present And I memorize response with jq ".eabKid" as eab_kid And I memorize response with jq ".eabSecret" as eab_secret - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{profile_id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{eab_kid}" with secret "{eab_secret}" as acme_account diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index b36a3e328..bee46c3fb 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -2,7 +2,7 @@ Feature: Challenge Scenario: Validate challenge Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr diff --git a/backend/bdd/features/pki/acme/nonce.feature b/backend/bdd/features/pki/acme/nonce.feature index 601c53a85..9a55ae284 100644 --- a/backend/bdd/features/pki/acme/nonce.feature +++ b/backend/bdd/features/pki/acme/nonce.feature @@ -8,7 +8,7 @@ Feature: Nonce Scenario Outline: Send a bad nonce to account endpoints Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr @@ -51,7 +51,7 @@ Feature: Nonce Scenario Outline: Send the same nonce twice Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And I memorize acme_account.uri with jq "capture("/(?[^/]+)$") | .id" as account_id When I create certificate signing request as csr diff --git a/backend/bdd/features/pki/acme/order.feature b/backend/bdd/features/pki/acme/order.feature index 9e8b47980..19f467f00 100644 --- a/backend/bdd/features/pki/acme/order.feature +++ b/backend/bdd/features/pki/acme/order.feature @@ -2,7 +2,7 @@ Feature: Order Scenario: Create a new order Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -22,7 +22,7 @@ Feature: Order Scenario: Create a new order with SANs Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -52,7 +52,7 @@ Feature: Order Scenario: Fetch an order Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account When I create certificate signing request as csr Then I add names to certificate signing request csr @@ -72,7 +72,7 @@ Feature: Order Scenario Outline: Create an order with invalid identifier types Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And I peak and memorize the next nonce as nonce When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" @@ -105,7 +105,7 @@ Feature: Order Scenario Outline: Create an order with invalid identifier values Given I have an ACME cert profile as "acme_profile" - When I have an ACME client connecting to {BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory + When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account And I peak and memorize the next nonce as nonce When I send a raw ACME request to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/new-order" @@ -129,7 +129,7 @@ Feature: Order And the value response with jq ".status" should be equal to 400 And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:unsupportedIdentifier" And the value response with jq ".detail" should be equal to "Invalid DNS identifier" - + Examples: Bad Identifier Vluaes | identifier_value | | 127.0.0.1 | diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 88c31c4a0..1d20cd9bd 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -191,23 +191,34 @@ def step_impl(context: Context, method: str, url: str): logger.debug("Response JSON payload: %r", response.json()) -@when("I have an ACME client connecting to {url}") -def step_impl(context: Context, url: str): - private_key = rsa.generate_private_key( - public_exponent=ACC_KEY_PUBLIC_EXPONENT, key_size=ACC_KEY_BITS - ) - pem_bytes = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - acc_jwk = JWKRSA.load(pem_bytes) +def create_acme_client(context: Context, url: str, acc_jwk: JWKRSA | None = None): + if acc_jwk is None: + private_key = rsa.generate_private_key( + public_exponent=ACC_KEY_PUBLIC_EXPONENT, key_size=ACC_KEY_BITS + ) + pem_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + acc_jwk = JWKRSA.load(pem_bytes) net = client.ClientNetwork(acc_jwk) directory_url = url.format(**context.vars) directory = client.ClientV2.get_directory(directory_url, net) context.acme_client = client.ClientV2(directory, net=net) +@when('I have an ACME client connecting to "{url}"') +def step_impl(context: Context, url: str): + create_acme_client(context, url) + + +@when('I have an ACME client connecting to "{url}" with the key pair from {client_var}') +def step_impl(context: Context, url: str, client_var: str): + another_client = eval_var(context, client_var, as_json=False) + create_acme_client(context, url, acc_jwk=another_client.net.key) + + @then('the response status code should be "{expected_status_code:d}"') def step_impl(context: Context, expected_status_code: int): assert context.vars["response"].status_code == expected_status_code, ( From d792a79da8f6062afe8043870701ffa406d68ca6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 23:05:00 -0800 Subject: [PATCH 29/80] Fix more tests --- backend/bdd/features/pki/acme/access-control.feature | 3 ++- backend/src/ee/services/pki-acme/pki-acme-fns.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 96da3f7c8..3bbc458db 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -126,7 +126,7 @@ Feature: Access Control "url": "", "kid": "{acme_account1.uri}" }, - "raw_payload": "" + "payload": {} } """ Then the value response.status_code should be equal to 404 @@ -140,6 +140,7 @@ Feature: Access Control | order | .authorizations[0].uri | auth_uri | {auth_uri} | | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | + Scenario Outline: Access resources across a different profile with the same key pair Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index 08659c853..d82877891 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -13,7 +13,7 @@ export const buildUrl = (profileId: string, path: string): string => { export const extractAccountIdFromKid = (kid: string, profileId: string): string => { const kidPrefix = buildUrl(profileId, "/accounts/"); if (!kid.startsWith(kidPrefix)) { - throw new AcmeMalformedError({ message: "KID must start with the profile account URL" }); + throw new AcmeAccountDoesNotExistError({ message: "KID must start with the profile account URL" }); } return z.string().uuid().parse(kid.slice(kidPrefix.length)); }; From a3e289954cf596fe734f8da71fc8b8eaab55e86e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 23:06:50 -0800 Subject: [PATCH 30/80] More test cases --- .../bdd/features/pki/acme/access-control.feature | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 3bbc458db..8bb8a1ea3 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -156,6 +156,20 @@ Feature: Access Control Then I create a RSA private key pair as cert_key Then I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format Then I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order + Then I peak and memorize the next nonce as nonce + Then I memorize with jq "" as + When I send a raw ACME request to "" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "", + "kid": "{acme_account0.uri}" + }, + "payload": {"invalid": "payload"} + } + """ # With original owner account under their profile, the invalid payload is going to trigger other errors instead of # 404, this is to make sure that our URLs are actually correct Then the value response.status_code should not be equal to 404 From 97b7e6c6bfd27cdc7a32c995561d94e01e197ac7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Thu, 6 Nov 2025 23:11:43 -0800 Subject: [PATCH 31/80] Fix broken tests --- backend/bdd/features/steps/pki_acme.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 1d20cd9bd..be4383b08 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -324,18 +324,15 @@ def send_raw_acme_req(context: Context, url: str): alg = acme_client.net.alg if "raw_payload" in content: encoded_payload = content["raw_payload"].encode("utf-8") + elif "payload" in content: + payload = ( + replace_vars(content["payload"], context.vars) + if "payload" in content + else None + ) + encoded_payload = json.dumps(payload).encode() if payload is not None else b"" else: - if "payload" not in content: - payload = ( - replace_vars(content["payload"], context.vars) - if "payload" in content - else None - ) - encoded_payload = ( - json.dumps(payload).encode() if payload is not None else b"" - ) - else: - encoded_payload = b"" + encoded_payload = b"" protected_headers = json.dumps(protected) signature = alg.sign( key=acme_client.net.key.key, From e5168e88e4b2db35bd78ae8af169a9b76a89ca7b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 11:01:20 -0800 Subject: [PATCH 32/80] Use re2 for identifier validation --- backend/src/ee/services/pki-acme/pki-acme-fns.ts | 9 +++++++++ backend/src/ee/services/pki-acme/pki-acme-schemas.ts | 12 ++---------- backend/src/ee/services/pki-acme/pki-acme-service.ts | 7 +++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index d82877891..eabfc7802 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -1,3 +1,5 @@ +import { getConfig } from "@app/lib/config/env"; +import RE2 from "re2"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; @@ -17,3 +19,10 @@ export const extractAccountIdFromKid = (kid: string, profileId: string): string } return z.string().uuid().parse(kid.slice(kidPrefix.length)); }; + +export const validateDnsIdentifier = (identifier: string): boolean => { + // DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen + const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/); + const labels = identifier.split("."); + return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label)); +}; diff --git a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts index baa686e59..58ca7e833 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-schemas.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-schemas.ts @@ -1,4 +1,3 @@ -import RE2 from "re2"; import { z } from "zod"; export enum AcmeIdentifierType { @@ -84,19 +83,12 @@ export const CreateAcmeAccountResponseSchema = z.object({ orders: z.string().optional() }); -export const ValidDNSIdentifierRegex = /^(?!-)[A-Za-z0-9-]{1,63}(? { - // DNS label pattern: 1-63 chars, alphanumeric or hyphen, but not starting or ending with hyphen - const labelPattern = new RE2(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/); - const labels = val.split("."); - return labels.every((label) => label.length >= 1 && label.length <= 63 && labelPattern.test(label)); - }, "Invalid DNS identifier") + type: z.string(), + value: z.string() }) ), notBefore: z.string().optional(), diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 3190f98ef..51f530264 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -43,7 +43,7 @@ import { AcmeServerInternalError, AcmeUnsupportedIdentifierError } from "./pki-acme-errors"; -import { buildUrl, extractAccountIdFromKid } from "./pki-acme-fns"; +import { buildUrl, extractAccountIdFromKid, validateDnsIdentifier } from "./pki-acme-fns"; import { TPkiAcmeOrderAuthDALFactory } from "./pki-acme-order-auth-dal"; import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal"; import { @@ -53,8 +53,7 @@ import { AcmeIdentifierType, AcmeOrderStatus, CreateAcmeAccountBodySchema, - ProtectedHeaderSchema, - ValidDNSIdentifierRegex + ProtectedHeaderSchema } from "./pki-acme-schemas"; import { TAcmeOrderResource, @@ -497,7 +496,7 @@ export const pkiAcmeServiceFactory = ({ if ( payload.identifiers.some( (identifier) => - !ValidDNSIdentifierRegex.test(identifier.value) || + !validateDnsIdentifier(identifier.value) || isPrivateIp(identifier.value) || (!getConfig().isDevelopmentMode && identifier.value.toLowerCase() === "localhost") ) From 1ad84f0450f0de30a14c36b16fc15f561cd35059 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 11:24:13 -0800 Subject: [PATCH 33/80] Provision a new profile automatically if not provided --- backend/bdd/features/steps/pki_acme.py | 35 +++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index be4383b08..33dc8c939 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -138,12 +138,39 @@ def step_impl(context: Context, faker_type: str, var_name: str): @given('I have an ACME cert profile as "{profile_var}"') def step_impl(context: Context, profile_var: str): - # TODO: Fixed value for now, just to make test much easier, - # we should call infisical API to create such profile instead - # in the future profile_id = os.getenv("PROFILE_ID") - kid = profile_id secret = os.getenv("EAB_SECRET") + if profile_id is None and secret is None: + kid = profile_id + + else: + profile_slug = faker.slug() + response = context.http_client.post( + "/api/v1/pki/certificate-profiles", + headers=prepare_headers(context), + json={ + "projectId": context.vars["PROJECT_ID"], + "slug": profile_slug, + "description": "ACME Profile created by BDD test", + "enrollmentType": "acme", + "caId": context.vars["CERT_CA_ID"], + "certificateTemplateId": context.vars["CERT_TEMPLATE_ID"], + "acmeConfig": {}, + }, + ) + response.raise_for_status() + resp_json = response.json() + profile_id = resp_json["certificateProfile"]["id"] + kid = profile_id + + response = context.http_client.get( + f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal", + headers=prepare_headers(context), + ) + response.raise_for_status() + resp_json = response.json() + secret = resp_json["eabSecret"] + context.vars[profile_var] = AcmeProfile( profile_id, eab_kid=kid, From 60c428c5bb27d9edbc6b95a35797d8150f227cc3 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 12:09:01 -0800 Subject: [PATCH 34/80] Fix provisioning profile --- backend/bdd/features/steps/pki_acme.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index 33dc8c939..a408c81d1 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -140,14 +140,14 @@ def step_impl(context: Context, faker_type: str, var_name: str): def step_impl(context: Context, profile_var: str): profile_id = os.getenv("PROFILE_ID") secret = os.getenv("EAB_SECRET") - if profile_id is None and secret is None: + if profile_id is not None and secret is not None: kid = profile_id - else: profile_slug = faker.slug() + jwt_token = context.vars["AUTH_TOKEN"] response = context.http_client.post( "/api/v1/pki/certificate-profiles", - headers=prepare_headers(context), + headers=dict(authorization="Bearer {}".format(jwt_token)), json={ "projectId": context.vars["PROJECT_ID"], "slug": profile_slug, @@ -165,7 +165,7 @@ def step_impl(context: Context, profile_var: str): response = context.http_client.get( f"/api/v1/pki/certificate-profiles/{profile_id}/acme/eab-secret/reveal", - headers=prepare_headers(context), + headers=dict(authorization="Bearer {}".format(jwt_token)), ) response.raise_for_status() resp_json = response.json() From eabfa3a7a95551353b3ab83182f39b268313f33c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 16:10:25 -0800 Subject: [PATCH 35/80] Bootstrap new Infisical instance for BDD tests --- backend/bdd/features/environment.py | 159 ++++++++++++++++-- .../features/pki/acme/access-control.feature | 4 +- 2 files changed, 151 insertions(+), 12 deletions(-) diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index b355f98ae..8efd257c8 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -3,6 +3,7 @@ import os import httpx from behave.runner import Context from dotenv import load_dotenv +from faker import Faker load_dotenv() @@ -11,16 +12,154 @@ PROJECT_ID = os.environ.get("PROJECT_ID") CERT_CA_ID = os.environ.get("CERT_CA_ID") CERT_TEMPLATE_ID = os.environ.get("CERT_TEMPLATE_ID") AUTH_TOKEN = os.environ.get("INFISICAL_TOKEN") +BOOTSTRAP_INFISICAL = int(os.environ.get("BOOTSTRAP_INFISICAL", 0)) + + +# Called mostly from a CI to setup the new Infisical instance to get it ready for BDD tests +def bootstrap_infisical(context: Context): + faker = Faker() + with httpx.Client(base_url=BASE_URL) as client: + resp = client.post( + "/api/v1/admin/signup", + json={ + "email": f"{faker.user_name()}@infisical.com", + "password": faker.password(), + "firstName": faker.first_name(), + "lastName": faker.last_name(), + }, + ) + resp.raise_for_status() + body = resp.json() + org = body["organization"] + user = body["user"] + auth_token = body["token"] + + project_slug = faker.slug() + resp = client.post( + "/api/v1/projects", + json={ + "projectName": project_slug, + "projectDescription": faker.paragraph(), + "template": "default", + "type": "cert-manager", + }, + ) + resp.raise_for_status() + body = resp.json() + project = body["project"] + + ca_slug = faker.slug() + resp = client.post( + "/api/v1/pki/ca/internal", + json={ + "projectId": project["id"], + "name": ca_slug, + "type": "internal", + "status": "active", + "enableDirectIssuance": True, + "configuration": { + "type": "root", + "organization": "Infisican Inc", + "ou": "", + "country": "", + "province": "", + "locality": "", + "commonName": "", + "notAfter": "2035-11-07", + "maxPathLength": -1, + "keyAlgorithm": "RSA_2048", + }, + }, + ) + resp.raise_for_status() + body = resp.json() + ca = body["certificateAuthorities"] + + cert_template_slug = faker.slug() + resp = client.post( + "/api/v2/certificate-templates", + json={ + "projectId": project["id"], + "name": cert_template_slug, + "description": "", + "subject": [{"type": "common_name", "allowed": ["*"]}], + "sans": [], + "keyUsages": { + "required": [], + "allowed": [ + "digital_signature", + "non_repudiation", + "key_encipherment", + "data_encipherment", + "key_agreement", + "key_cert_sign", + "crl_sign", + "encipher_only", + "decipher_only", + ], + }, + "extendedKeyUsages": { + "required": [], + "allowed": [ + "client_auth", + "server_auth", + "code_signing", + "email_protection", + "ocsp_signing", + "time_stamping", + ], + }, + "algorithms": { + "signature": [ + "SHA256-RSA", + "SHA512-RSA", + "SHA384-ECDSA", + "SHA384-RSA", + "SHA256-ECDSA", + "SHA512-ECDSA", + ], + "keyAlgorithm": [ + "RSA-2048", + "RSA-4096", + "ECDSA-P384", + "RSA-3072", + "ECDSA-P256", + "ECDSA-P521", + ], + }, + "validity": {"max": "365d"}, + }, + ) + resp.raise_for_status() + body = resp.json() + cert_template = body["certificateTemplate"] + + return dict( + org=org, + user=user, + project=project, + ca=ca, + cert_template=cert_template, + auth_token=auth_token, + ) def before_all(context: Context): - context.vars = { - "BASE_URL": BASE_URL, - "PROJECT_ID": PROJECT_ID, - "CERT_CA_ID": CERT_CA_ID, - "CERT_TEMPLATE_ID": CERT_TEMPLATE_ID, - "AUTH_TOKEN": AUTH_TOKEN, - } - context.http_client = httpx.Client( - base_url=BASE_URL, # headers={"Authorization": f"Bearer {AUTH_TOKEN}"} - ) + if BOOTSTRAP_INFISICAL: + details = bootstrap_infisical(context) + context.vars = { + "BASE_URL": BASE_URL, + "PROJECT_ID": details["project"]["id"], + "CERT_CA_ID": details["ca"]["id"], + "CERT_TEMPLATE_ID": details["cert_template"]["id"], + "AUTH_TOKEN": details["auth_token"], + } + else: + context.vars = { + "BASE_URL": BASE_URL, + "PROJECT_ID": PROJECT_ID, + "CERT_CA_ID": CERT_CA_ID, + "CERT_TEMPLATE_ID": CERT_TEMPLATE_ID, + "AUTH_TOKEN": AUTH_TOKEN, + } + context.http_client = httpx.Client(base_url=BASE_URL) diff --git a/backend/bdd/features/pki/acme/access-control.feature b/backend/bdd/features/pki/acme/access-control.feature index 8bb8a1ea3..6615d00f8 100644 --- a/backend/bdd/features/pki/acme/access-control.feature +++ b/backend/bdd/features/pki/acme/access-control.feature @@ -1,6 +1,6 @@ Feature: Access Control - Scenario Outline: Access across resources across different account + Scenario Outline: Access resources across different account Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 @@ -60,7 +60,7 @@ Feature: Access Control | order | .authorizations[0].uri | auth_uri | {auth_uri} | | | order | .authorizations[0].body.challenges[0].url | challenge_uri | {challenge_uri} | {} | - Scenario Outline: Access resources across a different profile + Scenario Outline: Access resources across a different profiles Given I have an ACME cert profile as "acme_profile" When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory" Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account0 From a641e82b296f30a4fbd453e2171823fa5d1e4e9b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 16:29:37 -0800 Subject: [PATCH 36/80] Save bootstrap result to avoid losing it --- backend/bdd/features/environment.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 8efd257c8..4a05ecf9e 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -1,11 +1,15 @@ +import json import os +import pathlib import httpx from behave.runner import Context from dotenv import load_dotenv from faker import Faker +import logging load_dotenv() +logger = logging.getLogger(__name__) BASE_URL = os.environ.get("INFISICAL_API_URL", "http://localhost:8080") PROJECT_ID = os.environ.get("PROJECT_ID") @@ -17,6 +21,13 @@ BOOTSTRAP_INFISICAL = int(os.environ.get("BOOTSTRAP_INFISICAL", 0)) # Called mostly from a CI to setup the new Infisical instance to get it ready for BDD tests def bootstrap_infisical(context: Context): + bootstrap_result_file = pathlib.Path.cwd() / ".bootstrap-result.json" + if bootstrap_result_file.exists(): + logger.info( + "Bootstrap result file exists at %s, loading it now", bootstrap_result_file + ) + return json.loads(bootstrap_result_file.read_text()) + faker = Faker() with httpx.Client(base_url=BASE_URL) as client: resp = client.post( @@ -33,10 +44,12 @@ def bootstrap_infisical(context: Context): org = body["organization"] user = body["user"] auth_token = body["token"] + headers = dict(authorization=f"Bearer {auth_token}") project_slug = faker.slug() resp = client.post( "/api/v1/projects", + headers=headers, json={ "projectName": project_slug, "projectDescription": faker.paragraph(), @@ -51,6 +64,7 @@ def bootstrap_infisical(context: Context): ca_slug = faker.slug() resp = client.post( "/api/v1/pki/ca/internal", + headers=headers, json={ "projectId": project["id"], "name": ca_slug, @@ -78,6 +92,7 @@ def bootstrap_infisical(context: Context): cert_template_slug = faker.slug() resp = client.post( "/api/v2/certificate-templates", + headers=headers, json={ "projectId": project["id"], "name": cert_template_slug, @@ -134,7 +149,7 @@ def bootstrap_infisical(context: Context): body = resp.json() cert_template = body["certificateTemplate"] - return dict( + bootstrap_result = dict( org=org, user=user, project=project, @@ -142,6 +157,8 @@ def bootstrap_infisical(context: Context): cert_template=cert_template, auth_token=auth_token, ) + bootstrap_result_file.write_text(json.dumps(bootstrap_result)) + return bootstrap_result def before_all(context: Context): From 3b5046eadd44a72415c32ebbd9119e9a46baa7cd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 16:50:12 -0800 Subject: [PATCH 37/80] Add new token --- backend/bdd/features/environment.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index 4a05ecf9e..c77646358 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -43,6 +43,15 @@ def bootstrap_infisical(context: Context): body = resp.json() org = body["organization"] user = body["user"] + temp_token = body["token"] + + resp = client.post( + "/api/v1/auth/token", + headers={"Authorization": f"Bearer {temp_token}"}, + json={}, + ) + resp.raise_for_status() + body = resp.json() auth_token = body["token"] headers = dict(authorization=f"Bearer {auth_token}") From c4b759b6a87614bd8c8babcd7174ea0cd73d5cfd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:09:29 -0800 Subject: [PATCH 38/80] Fix bootstrap auth --- .gitignore | 1 + backend/bdd/features/environment.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f2a23324b..b4e9a07c2 100644 --- a/.gitignore +++ b/.gitignore @@ -71,5 +71,6 @@ frontend-build cli/infisical-merge cli/test/infisical-merge /backend/binary +backend/bdd/.bdd-infisical-bootstrap-result.json /npm/bin diff --git a/backend/bdd/features/environment.py b/backend/bdd/features/environment.py index c77646358..52615036a 100644 --- a/backend/bdd/features/environment.py +++ b/backend/bdd/features/environment.py @@ -21,7 +21,7 @@ BOOTSTRAP_INFISICAL = int(os.environ.get("BOOTSTRAP_INFISICAL", 0)) # Called mostly from a CI to setup the new Infisical instance to get it ready for BDD tests def bootstrap_infisical(context: Context): - bootstrap_result_file = pathlib.Path.cwd() / ".bootstrap-result.json" + bootstrap_result_file = pathlib.Path.cwd() / ".bdd-infisical-bootstrap-result.json" if bootstrap_result_file.exists(): logger.info( "Bootstrap result file exists at %s, loading it now", bootstrap_result_file @@ -45,6 +45,15 @@ def bootstrap_infisical(context: Context): user = body["user"] temp_token = body["token"] + resp = client.post( + "/api/v3/auth/select-organization", + headers={"Authorization": f"Bearer {temp_token}"}, + json={"organizationId": org["id"]}, + ) + resp.raise_for_status() + body = resp.json() + temp_token = body["token"] + resp = client.post( "/api/v1/auth/token", headers={"Authorization": f"Bearer {temp_token}"}, @@ -96,7 +105,7 @@ def bootstrap_infisical(context: Context): ) resp.raise_for_status() body = resp.json() - ca = body["certificateAuthorities"] + ca = body cert_template_slug = faker.slug() resp = client.post( From b5a88a83a5faa830595e6fe99f59649d577297d6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:24:29 -0800 Subject: [PATCH 39/80] Add bdd on github --- .github/resources/.env.bdd | 4 ++ .github/workflows/run-backend-bdd-tests.yml | 57 +++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 .github/resources/.env.bdd create mode 100644 .github/workflows/run-backend-bdd-tests.yml diff --git a/.github/resources/.env.bdd b/.github/resources/.env.bdd new file mode 100644 index 000000000..f6c087c9d --- /dev/null +++ b/.github/resources/.env.bdd @@ -0,0 +1,4 @@ +ACME_DEVELOPMENT_MODE=true +ACME_FEATURE_ENABLED=true +# TODO: fix the wrong override +ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES='{"localhost": "192.168.50.215:8087"}' diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml new file mode 100644 index 000000000..713dc5387 --- /dev/null +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -0,0 +1,57 @@ +name: "Run backend BDD tests" + +on: + pull_request: + types: [opened, synchronize] + paths: + - "backend/**" + - "!backend/README.md" + - "!backend/.*" + - "backend/.eslintrc.js" + workflow_call: + +jobs: + check-be-pr: + name: Run BDD tests + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + docker system prune -af + + - name: ☁️ Checkout source + uses: actions/checkout@v3 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Install Python + run: uv python install + - uses: KengoTODA/actions-setup-docker-compose@v1 + if: ${{ env.ACT }} + name: Install `docker compose` for local simulations + with: + version: "2.14.2" + - name: 🔧 Setup Node 20 + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: backend/package-lock.json + - name: Install dependencies + run: npm install + working-directory: backend + - name: Start Infisical + run: cp .github/resources/.env.bdd .env && docker compose -f docker-compose.dev.yml up + - name: Run bdd tests + run: npm run test:bdd + working-directory: backend + env: + INFISICAL_API_URL: http://localhost:8080 + BOOTSTRAP_INFISICAL: "1" + - name: cleanup + run: | + docker compose -f "docker-compose.dev.yml" down From 6aac457fbd1a88ce9fd1b748f83be99619da78e2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:31:47 -0800 Subject: [PATCH 40/80] Run compose as daemon, wait for the server online --- .github/workflows/run-backend-bdd-tests.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 713dc5387..43e744fde 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -45,7 +45,14 @@ jobs: run: npm install working-directory: backend - name: Start Infisical - run: cp .github/resources/.env.bdd .env && docker compose -f docker-compose.dev.yml up + run: cp .github/resources/.env.bdd .env && docker compose -f docker-compose.dev.yml up -d + - name: Wait for service to return 200 + uses: nev7n/wait_for_response@v1 + with: + url: "http://localhost:8080/api/v1/admin/config" # Replace with your service URL + responseCode: "200" + timeout: 60000 + interval: 500 - name: Run bdd tests run: npm run test:bdd working-directory: backend From ac3c857d2f3cc5ce286642edab43190af72b0136 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:36:09 -0800 Subject: [PATCH 41/80] Build docker-compose img files --- .github/workflows/run-backend-bdd-tests.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 43e744fde..9de8e233e 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -44,6 +44,15 @@ jobs: - name: Install dependencies run: npm install working-directory: backend + - name: Build Infisical backend Docker image with caching + uses: docker/bake-action@v5 + with: + files: docker-compose.dev.yml + targets: backend + set: | + *.cache-from: type=gha,scope=${{ github.ref_name }}-compose # Import cache key (unique per branch) + *.cache-to: type=gha,scope=${{ github.ref_name }}-compose,mode=max # Export full cache + load: true - name: Start Infisical run: cp .github/resources/.env.bdd .env && docker compose -f docker-compose.dev.yml up -d - name: Wait for service to return 200 From a1566cb2efbb2dd49560817db59a67806371c900 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:43:12 -0800 Subject: [PATCH 42/80] Try to fix docker build --- .github/workflows/run-backend-bdd-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 9de8e233e..fbd1120f8 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -44,6 +44,9 @@ jobs: - name: Install dependencies run: npm install working-directory: backend + - name: Copy .env.bdd to .env + run: .github/resources/.env.bdd .env + working-directory: backend - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 with: @@ -54,7 +57,7 @@ jobs: *.cache-to: type=gha,scope=${{ github.ref_name }}-compose,mode=max # Export full cache load: true - name: Start Infisical - run: cp .github/resources/.env.bdd .env && docker compose -f docker-compose.dev.yml up -d + run: docker compose -f docker-compose.dev.yml up -d - name: Wait for service to return 200 uses: nev7n/wait_for_response@v1 with: From 9ea1614da06c820bc94c6086802c488276612495 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 17:58:02 -0800 Subject: [PATCH 43/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index fbd1120f8..51ccaf853 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -45,7 +45,7 @@ jobs: run: npm install working-directory: backend - name: Copy .env.bdd to .env - run: .github/resources/.env.bdd .env + run: cp .github/resources/.env.bdd .env working-directory: backend - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 From 4a9f61bed66050c58077b491842af5931b712c8a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:10:46 -0800 Subject: [PATCH 44/80] Try to fix bdd --- .github/workflows/run-backend-bdd-tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 51ccaf853..7ce2dab93 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -25,7 +25,10 @@ jobs: docker system prune -af - name: ☁️ Checkout source - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + fetch-tags: true + fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install Python From 550e5158cc4ee9608cdf01fedb798abefe74b0e9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:16:38 -0800 Subject: [PATCH 45/80] CI --- .github/workflows/run-backend-bdd-tests.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 7ce2dab93..90e5af91f 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -27,8 +27,13 @@ jobs: - name: ☁️ Checkout source uses: actions/checkout@v4 with: - fetch-tags: true - fetch-depth: 0 + sparse-checkout: | + .github/resources + sparse-checkout-cone-mode: false + # XXX: + - name: Copy .env.bdd to .env + run: cp .github/resources/.env.bdd .env + working-directory: backend - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install Python @@ -47,9 +52,7 @@ jobs: - name: Install dependencies run: npm install working-directory: backend - - name: Copy .env.bdd to .env - run: cp .github/resources/.env.bdd .env - working-directory: backend + - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 with: From 4a9cf139431dc64a9c6b86e3f249a2b4eeb10efd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:20:52 -0800 Subject: [PATCH 46/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 90e5af91f..8c6ddade5 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -26,10 +26,6 @@ jobs: - name: ☁️ Checkout source uses: actions/checkout@v4 - with: - sparse-checkout: | - .github/resources - sparse-checkout-cone-mode: false # XXX: - name: Copy .env.bdd to .env run: cp .github/resources/.env.bdd .env From 5a555fc849436a10eeb12ab066ad415bd678ef0d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:28:41 -0800 Subject: [PATCH 47/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 8c6ddade5..d6c02c2e6 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -26,7 +26,11 @@ jobs: - name: ☁️ Checkout source uses: actions/checkout@v4 - # XXX: + with: + sparse-checkout: | + . + .github/resources + - name: Copy .env.bdd to .env run: cp .github/resources/.env.bdd .env working-directory: backend From 3b84fc3e9e4d8288563e542755a83583094e1a8f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:31:35 -0800 Subject: [PATCH 48/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index d6c02c2e6..5c26e073b 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -30,10 +30,8 @@ jobs: sparse-checkout: | . .github/resources - - name: Copy .env.bdd to .env run: cp .github/resources/.env.bdd .env - working-directory: backend - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install Python From 1f89d1b19cdeb081c6e42002ae4c81594ef097b9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:39:00 -0800 Subject: [PATCH 49/80] Just write .env --- .github/workflows/run-backend-bdd-tests.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 5c26e073b..5ed8f668e 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -25,13 +25,7 @@ jobs: docker system prune -af - name: ☁️ Checkout source - uses: actions/checkout@v4 - with: - sparse-checkout: | - . - .github/resources - - name: Copy .env.bdd to .env - run: cp .github/resources/.env.bdd .env + uses: actions/checkout@v3 - name: Install uv uses: astral-sh/setup-uv@v5 - name: Install Python @@ -51,6 +45,11 @@ jobs: run: npm install working-directory: backend + - name: Output .env file + run: | + echo "ACME_DEVELOPMENT_MODE=true" > .env + echo "ACME_FEATURE_ENABLED=true" >> .env + echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 with: From 459edd14ce0169d4480736327939ee1a554f33a6 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 18:44:11 -0800 Subject: [PATCH 50/80] CI --- .github/workflows/run-backend-bdd-tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 5ed8f668e..7aeb3fbf0 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -55,9 +55,6 @@ jobs: with: files: docker-compose.dev.yml targets: backend - set: | - *.cache-from: type=gha,scope=${{ github.ref_name }}-compose # Import cache key (unique per branch) - *.cache-to: type=gha,scope=${{ github.ref_name }}-compose,mode=max # Export full cache load: true - name: Start Infisical run: docker compose -f docker-compose.dev.yml up -d From 185f3cc7bfd3af678f31393f416c422d190df759 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:04:13 -0800 Subject: [PATCH 51/80] Fix wait service --- .github/workflows/run-backend-bdd-tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 7aeb3fbf0..0cb2c9875 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -58,13 +58,13 @@ jobs: load: true - name: Start Infisical run: docker compose -f docker-compose.dev.yml up -d - - name: Wait for service to return 200 - uses: nev7n/wait_for_response@v1 + - name: Wait for API to be ready + uses: nick-fields/retry@v3 with: - url: "http://localhost:8080/api/v1/admin/config" # Replace with your service URL - responseCode: "200" - timeout: 60000 - interval: 500 + timeout_seconds: 60 + max_attempts: 12 + command: | + curl -f -X GET http://localhost:8080/api/v1/admin/config - name: Run bdd tests run: npm run test:bdd working-directory: backend From 81c88fffec2ef0efdc45f94dcdfb31295cb4f628 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:13:27 -0800 Subject: [PATCH 52/80] Cache build --- .github/workflows/run-backend-bdd-tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 0cb2c9875..0c537a9de 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -55,7 +55,11 @@ jobs: with: files: docker-compose.dev.yml targets: backend - load: true + push: true # Or false if just building locally + no-cache: false + set: | + *.cache-from=type=gha,scope=infisical-backend-bdd-tests + *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests - name: Start Infisical run: docker compose -f docker-compose.dev.yml up -d - name: Wait for API to be ready From e9f88e28be54497adf51ba46bc828ec58146fda2 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:20:09 -0800 Subject: [PATCH 53/80] More attempts --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 0c537a9de..93b0f977f 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -66,7 +66,7 @@ jobs: uses: nick-fields/retry@v3 with: timeout_seconds: 60 - max_attempts: 12 + max_attempts: 30 command: | curl -f -X GET http://localhost:8080/api/v1/admin/config - name: Run bdd tests From e5322c77f75d192c7f37642dc3366d436b9134a8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:20:35 -0800 Subject: [PATCH 54/80] Run backend only --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 93b0f977f..a9ca2369e 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -61,7 +61,7 @@ jobs: *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests - name: Start Infisical - run: docker compose -f docker-compose.dev.yml up -d + run: docker compose -f docker-compose.dev.yml up -d backend - name: Wait for API to be ready uses: nick-fields/retry@v3 with: From bc5a6c6d6734fa1f1a2f6abc2a6f6f3a1f932bb7 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:23:37 -0800 Subject: [PATCH 55/80] ci... --- .github/workflows/run-backend-bdd-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index a9ca2369e..ae51c4997 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -50,6 +50,11 @@ jobs: echo "ACME_DEVELOPMENT_MODE=true" > .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: | + image=moby/buildkit:latest - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 with: From 58cdd8e0dc4a72d9cc7dd34caa9b83f2ea31f26f Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:29:27 -0800 Subject: [PATCH 56/80] CI --- .github/workflows/run-backend-bdd-tests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index ae51c4997..d605804bc 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -60,8 +60,7 @@ jobs: with: files: docker-compose.dev.yml targets: backend - push: true # Or false if just building locally - no-cache: false + # no-cache: false set: | *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests From 8e9e4f4130c3fae9e130d30eaa551a4b6c03af80 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 19:46:47 -0800 Subject: [PATCH 57/80] Load to local docker --- .github/workflows/run-backend-bdd-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index d605804bc..d7709e629 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -60,6 +60,7 @@ jobs: with: files: docker-compose.dev.yml targets: backend + load: true # no-cache: false set: | *.cache-from=type=gha,scope=infisical-backend-bdd-tests From 2cf69a408307f5592a560a3ae5fb1ba68b7c8acb Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 20:10:39 -0800 Subject: [PATCH 58/80] See how it goes for the up op --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index d7709e629..4f3dd0179 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -66,7 +66,7 @@ jobs: *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests - name: Start Infisical - run: docker compose -f docker-compose.dev.yml up -d backend + run: docker compose -f docker-compose.dev.yml up backend - name: Wait for API to be ready uses: nick-fields/retry@v3 with: From 433b573c8cfcf3dda7e4446f370ea57d12fb5f02 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 20:18:52 -0800 Subject: [PATCH 59/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 4f3dd0179..0c1637cc5 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -47,7 +47,8 @@ jobs: - name: Output .env file run: | - echo "ACME_DEVELOPMENT_MODE=true" > .env + cp .env.example .env + echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env - name: Set up Docker Buildx From 023437c19760ae82449cac7a187996765011b6ac Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 20:30:23 -0800 Subject: [PATCH 60/80] ci --- .github/workflows/run-backend-bdd-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 0c1637cc5..bc896fddf 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -48,6 +48,7 @@ jobs: - name: Output .env file run: | cp .env.example .env + sed -i 's/^ENCRYPTION_KEY=.*$/ENCRYPTION_KEY=9DEgoAeERSd4uBYeCV5iuCHMhnpL5QQFdX+j1nTrkrc==/' .env echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env From 572ac95801a2b12659b27c884b7f9115e7e288ea Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 20:47:03 -0800 Subject: [PATCH 61/80] Try to fix enc key --- .github/workflows/run-backend-bdd-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index bc896fddf..c84e2c0b3 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -48,7 +48,8 @@ jobs: - name: Output .env file run: | cp .env.example .env - sed -i 's/^ENCRYPTION_KEY=.*$/ENCRYPTION_KEY=9DEgoAeERSd4uBYeCV5iuCHMhnpL5QQFdX+j1nTrkrc==/' .env + NEW_ENCRYPTION_KEY=$(openssl rand -base64 32) + sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY/" .env echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env From 64383f59182d181fe9fda55cacaf1ff159ab009c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Fri, 7 Nov 2025 20:56:04 -0800 Subject: [PATCH 62/80] CI --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index c84e2c0b3..a8bacf9dc 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -69,7 +69,7 @@ jobs: *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests - name: Start Infisical - run: docker compose -f docker-compose.dev.yml up backend + run: docker compose -f docker-compose.dev.yml up -d - name: Wait for API to be ready uses: nick-fields/retry@v3 with: From 87a6d2cf869d01a23230b4b4af486abd7787f347 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 09:46:34 -0800 Subject: [PATCH 63/80] Try to fix sed --- .github/workflows/run-backend-bdd-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index a8bacf9dc..51f6c2f91 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -11,7 +11,7 @@ on: workflow_call: jobs: - check-be-pr: + run-backend-bdd-tests: name: Run BDD tests runs-on: ubuntu-latest timeout-minutes: 15 @@ -49,7 +49,7 @@ jobs: run: | cp .env.example .env NEW_ENCRYPTION_KEY=$(openssl rand -base64 32) - sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY/" .env + sed -i "s#ENCRYPTION_KEY=.*#ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY#" .env echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env From 4c5778cdaf53b757853f2e5508f81576e43f2ca4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 09:59:56 -0800 Subject: [PATCH 64/80] Upload logs --- .github/workflows/run-backend-bdd-tests.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 51f6c2f91..27733c729 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -86,3 +86,16 @@ jobs: - name: cleanup run: | docker compose -f "docker-compose.dev.yml" down + - name: Dump backend logs + if: always() # Ensures this runs even if previous steps fail + run: | + mkdir -p logs + docker compose logs backend > logs/backend.log 2>&1 || true + - name: Upload backend logs as artifact + if: always() # Always upload, even on failure/cancellation + uses: actions/upload-artifact@v4 + with: + name: backend-logs-${{ github.run_id }} + path: logs/backend.log + retention-days: 7 + if-no-files-found: warn From b04973af56ca44f688116142ecf139a3af15147c Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 10:12:31 -0800 Subject: [PATCH 65/80] Fix log dump --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 27733c729..f314ff8bc 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -90,7 +90,7 @@ jobs: if: always() # Ensures this runs even if previous steps fail run: | mkdir -p logs - docker compose logs backend > logs/backend.log 2>&1 || true + docker compose -f docker-compose.dev.yml logs backend > logs/backend.log 2>&1 || true - name: Upload backend logs as artifact if: always() # Always upload, even on failure/cancellation uses: actions/upload-artifact@v4 From 6b7e3599553203a91168fb13380c6d7674223fb4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 10:38:21 -0800 Subject: [PATCH 66/80] Try to fix pg issue --- .github/workflows/run-backend-bdd-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index f314ff8bc..954c6c076 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -53,6 +53,9 @@ jobs: echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env + # XXX: This is a workaround for the error: "error:0308010C:digital envelope routines::unsupported" + # ref: https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported/69699772#69699772 + echo "NODE_OPTIONS=--openssl-legacy-provider" >> .env - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: From 61c496084512efa27c34440e203b3ef95a465679 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 11:35:06 -0800 Subject: [PATCH 67/80] Rebuild --- .github/workflows/run-backend-bdd-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 954c6c076..8b365ec9a 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -67,7 +67,8 @@ jobs: files: docker-compose.dev.yml targets: backend load: true - # no-cache: false + # Uncomment this to force a rebuild of the image + no-cache: true set: | *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests From 5ea887b61269056008d1f8bcf1f86d9bfe73bddc Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 12:08:28 -0800 Subject: [PATCH 68/80] cache --- .github/workflows/run-backend-bdd-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 8b365ec9a..90f124597 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -68,7 +68,7 @@ jobs: targets: backend load: true # Uncomment this to force a rebuild of the image - no-cache: true + # no-cache: true set: | *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests From bea8ea828e2851ac11c718944c4193a9b1202ba4 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 12:46:58 -0800 Subject: [PATCH 69/80] Increase timeout --- .github/workflows/run-backend-bdd-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 90f124597..768a6516f 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -63,6 +63,7 @@ jobs: image=moby/buildkit:latest - name: Build Infisical backend Docker image with caching uses: docker/bake-action@v5 + timeout-minutes: 30 with: files: docker-compose.dev.yml targets: backend From 8e1209092d236d9821f321da28d02b00ee5c094b Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 14:22:34 -0800 Subject: [PATCH 70/80] Use a new bdd docker compose --- .github/workflows/run-backend-bdd-tests.yml | 8 +-- docker-compose.bdd.yml | 69 +++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 docker-compose.bdd.yml diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 768a6516f..734779a50 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -65,7 +65,7 @@ jobs: uses: docker/bake-action@v5 timeout-minutes: 30 with: - files: docker-compose.dev.yml + files: docker-compose.bdd.yml targets: backend load: true # Uncomment this to force a rebuild of the image @@ -74,7 +74,7 @@ jobs: *.cache-from=type=gha,scope=infisical-backend-bdd-tests *.cache-to=type=gha,mode=max,scope=infisical-backend-bdd-tests - name: Start Infisical - run: docker compose -f docker-compose.dev.yml up -d + run: docker compose -f docker-compose.bdd.yml up -d - name: Wait for API to be ready uses: nick-fields/retry@v3 with: @@ -90,12 +90,12 @@ jobs: BOOTSTRAP_INFISICAL: "1" - name: cleanup run: | - docker compose -f "docker-compose.dev.yml" down + docker compose -f "docker-compose.bdd.yml" down - name: Dump backend logs if: always() # Ensures this runs even if previous steps fail run: | mkdir -p logs - docker compose -f docker-compose.dev.yml logs backend > logs/backend.log 2>&1 || true + docker compose -f docker-compose.bdd.yml logs backend > logs/backend.log 2>&1 || true - name: Upload backend logs as artifact if: always() # Always upload, even on failure/cancellation uses: actions/upload-artifact@v4 diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml new file mode 100644 index 000000000..815c9cf47 --- /dev/null +++ b/docker-compose.bdd.yml @@ -0,0 +1,69 @@ +version: "3.9" + +services: + nginx: + container_name: infisical-dev-nginx + image: nginx + restart: "always" + ports: + - 8080:80 + - 8443:443 + volumes: + - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - backend + + db: + image: postgres:14-alpine + ports: + - "5432:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: infisical + POSTGRES_USER: infisical + POSTGRES_DB: infisical + + redis: + image: redis + container_name: infisical-dev-redis + environment: + - ALLOW_EMPTY_PASSWORD=yes + ports: + - 6379:6379 + volumes: + - redis_data:/data + + + backend: + container_name: infisical-dev-api + build: + context: ./backend + dockerfile: Dockerfile.dev + depends_on: + db: + condition: service_started + redis: + condition: service_started + env_file: + - .env + ports: + - 4000:4000 + - 9464:9464 # for OTEL collection of Prometheus metrics + environment: + - NODE_ENV=development + - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + - TELEMETRY_ENABLED=false + volumes: + - ./backend/src:/app/src + - softhsm_tokens:/etc/softhsm2/tokens # SoftHSM tokens are stored in a volume to persist across container restarts + extra_hosts: + - "host.docker.internal:host-gateway" + +volumes: + postgres-data: + driver: local + redis_data: + driver: local + softhsm_tokens: + driver: local \ No newline at end of file From b6216f075ece5969bef8cc5e203755b58078d948 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 14:26:20 -0800 Subject: [PATCH 71/80] Do not write enc key --- .github/workflows/run-backend-bdd-tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 734779a50..7d1d9fff8 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -48,8 +48,6 @@ jobs: - name: Output .env file run: | cp .env.example .env - NEW_ENCRYPTION_KEY=$(openssl rand -base64 32) - sed -i "s#ENCRYPTION_KEY=.*#ENCRYPTION_KEY=$NEW_ENCRYPTION_KEY#" .env echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env From 500786a9aa69dfcc4a9fea26189464173b7fb06e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 14:43:16 -0800 Subject: [PATCH 72/80] ci --- docker-compose.bdd.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docker-compose.bdd.yml b/docker-compose.bdd.yml index 815c9cf47..dfee5f6b2 100644 --- a/docker-compose.bdd.yml +++ b/docker-compose.bdd.yml @@ -2,7 +2,7 @@ version: "3.9" services: nginx: - container_name: infisical-dev-nginx + container_name: infisical-bdd-nginx image: nginx restart: "always" ports: @@ -12,6 +12,7 @@ services: - ./nginx/default.dev.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend + - frontend db: image: postgres:14-alpine @@ -26,7 +27,7 @@ services: redis: image: redis - container_name: infisical-dev-redis + container_name: infisical-bdd-redis environment: - ALLOW_EMPTY_PASSWORD=yes ports: @@ -36,7 +37,7 @@ services: backend: - container_name: infisical-dev-api + container_name: infisical-bdd-api build: context: ./backend dockerfile: Dockerfile.dev @@ -60,6 +61,20 @@ services: extra_hosts: - "host.docker.internal:host-gateway" + # TODO: not really needed, but it seems like nginx needs it to be present + frontend: + container_name: infisical-bdd-frontend + restart: unless-stopped + depends_on: + - backend + build: + context: ./frontend + dockerfile: Dockerfile.dev + volumes: + - ./frontend/src:/app/src/ # mounted whole src to avoid missing reload on new files + - ./frontend/public:/app/public + env_file: .env + volumes: postgres-data: driver: local From 3b023cea7bb87cdfbc93caacca4beae8ceb952b9 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:02:06 -0800 Subject: [PATCH 73/80] Add bdd tests --- backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/package.json b/backend/package.json index baa81e65b..7a5efcb78 100644 --- a/backend/package.json +++ b/backend/package.json @@ -44,6 +44,7 @@ "test:e2e": "vitest run -c vitest.e2e.config.mts --bail=1", "test:e2e-watch": "vitest -c vitest.e2e.config.mts --bail=1", "test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.mts", + "test:bdd": "cd bdd && uv run behave", "generate:component": "tsx ./scripts/create-backend-file.ts", "generate:schema": "tsx ./scripts/generate-schema-types.ts && eslint --fix --ext ts ./src/db/schemas", "auditlog-migration:latest": "node ./dist/db/rename-migrations-to-mjs.mjs && knex --knexfile ./dist/db/auditlog-knexfile.mjs --client pg migrate:latest", From 5a47ac3a9daeafbd3e30ef4c4062506d9e63483d Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:19:08 -0800 Subject: [PATCH 74/80] Fix broken stuff caused by rebase --- backend/src/ee/services/pki-acme/pki-acme-fns.ts | 5 +---- backend/src/ee/services/pki-acme/pki-acme-service.ts | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-fns.ts b/backend/src/ee/services/pki-acme/pki-acme-fns.ts index eabfc7802..cc7ddb9b1 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-fns.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-fns.ts @@ -1,10 +1,7 @@ import { getConfig } from "@app/lib/config/env"; import RE2 from "re2"; import { z } from "zod"; - -import { getConfig } from "@app/lib/config/env"; - -import { AcmeMalformedError } from "./pki-acme-errors"; +import { AcmeAccountDoesNotExistError } from "./pki-acme-errors"; export const buildUrl = (profileId: string, path: string): string => { const appCfg = getConfig(); diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 51f530264..9e5491ecb 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -13,21 +13,22 @@ import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts"; import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { isPrivateIp } from "@app/lib/ip/ipRange"; import { logger } from "@app/lib/logger"; import { ActorType } from "@app/services/auth/auth-type"; -import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType, TCertificateProfileWithConfigs } from "@app/services/certificate-profile/certificate-profile-types"; import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; +import { getConfig } from "@app/lib/config/env"; import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal"; import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal"; import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal"; From 7de071736e37b40550f84770fedf11dd39a9fce0 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:41:33 -0800 Subject: [PATCH 75/80] Fix TTL for issuing cert --- backend/bdd/features/steps/pki_acme.py | 5 ++--- backend/src/ee/services/pki-acme/pki-acme-service.ts | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index a408c81d1..d9004e0ba 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -1,6 +1,5 @@ import json import logging -import os import re import threading import urllib.parse @@ -138,8 +137,8 @@ def step_impl(context: Context, faker_type: str, var_name: str): @given('I have an ACME cert profile as "{profile_var}"') def step_impl(context: Context, profile_var: str): - profile_id = os.getenv("PROFILE_ID") - secret = os.getenv("EAB_SECRET") + profile_id = context.vars.get("PROFILE_ID") + secret = context.vars.get("EAB_SECRET") if profile_id is not None and secret is not None: kid = profile_id else: diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 9e5491ecb..923ac94b4 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -641,11 +641,12 @@ export const pkiAcmeServiceFactory = ({ notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined, validity: !finalizingOrder.notAfter ? { + // 47 days, the default TTL comes with Let's Encrypt // TODO: read config from the profile to get the expiration time instead - ttl: (24 * 60 * 60 * 1000).toString() + ttl: `${47 * 24 * 60}m` } : // ttl is not used if notAfter is provided - ({ ttl: "0" } as const), + ({ ttl: "0m" } as const), enrollmentType: EnrollmentType.ACME }); // TODO: associate the certificate with the order From e6d2067c6ac80b2ee7e480ae4d8753cd6af5b6dd Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:46:10 -0800 Subject: [PATCH 76/80] Fix TTL error --- backend/src/ee/services/pki-acme/pki-acme-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/pki-acme/pki-acme-service.ts b/backend/src/ee/services/pki-acme/pki-acme-service.ts index 923ac94b4..fa0d0ff66 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-service.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-service.ts @@ -643,10 +643,10 @@ export const pkiAcmeServiceFactory = ({ ? { // 47 days, the default TTL comes with Let's Encrypt // TODO: read config from the profile to get the expiration time instead - ttl: `${47 * 24 * 60}m` + ttl: `${47}d` } : // ttl is not used if notAfter is provided - ({ ttl: "0m" } as const), + ({ ttl: "0d" } as const), enrollmentType: EnrollmentType.ACME }); // TODO: associate the certificate with the order From a275d38011638377ef782cb2b2a960d2730256af Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:55:43 -0800 Subject: [PATCH 77/80] Fix linter error --- backend/src/ee/routes/v1/pki-acme-router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/ee/routes/v1/pki-acme-router.ts b/backend/src/ee/routes/v1/pki-acme-router.ts index 627537c44..d58790039 100644 --- a/backend/src/ee/routes/v1/pki-acme-router.ts +++ b/backend/src/ee/routes/v1/pki-acme-router.ts @@ -261,7 +261,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { req }); if (payload !== "") { - throw new AcmeMalformedError({ detail: "Payload should be empty" }); + throw new AcmeMalformedError({ message: "Payload should be empty" }); } return sendAcmeResponse( res, @@ -373,7 +373,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { req }); if (payload !== "") { - throw new AcmeMalformedError({ detail: "Payload should be empty" }); + throw new AcmeMalformedError({ message: "Payload should be empty" }); } res.type("application/pem-certificate-chain"); return sendAcmeResponse( @@ -407,7 +407,7 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => { handler: async (req, res) => { const { profileId, accountId, payload } = await validateExistingAccount({ req }); if (payload !== "") { - throw new AcmeMalformedError({ detail: "Payload should be empty" }); + throw new AcmeMalformedError({ message: "Payload should be empty" }); } return sendAcmeResponse( res, From 6949d1feca32e4687165be9a52840d159a84e86a Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:56:27 -0800 Subject: [PATCH 78/80] Remove unused file --- .github/resources/.env.bdd | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/resources/.env.bdd diff --git a/.github/resources/.env.bdd b/.github/resources/.env.bdd deleted file mode 100644 index f6c087c9d..000000000 --- a/.github/resources/.env.bdd +++ /dev/null @@ -1,4 +0,0 @@ -ACME_DEVELOPMENT_MODE=true -ACME_FEATURE_ENABLED=true -# TODO: fix the wrong override -ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES='{"localhost": "192.168.50.215:8087"}' From 5aaa0ea67f8820e5fee25260a70fe107c70b300e Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 15:57:00 -0800 Subject: [PATCH 79/80] Remove unused stuff --- .github/workflows/run-backend-bdd-tests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/run-backend-bdd-tests.yml b/.github/workflows/run-backend-bdd-tests.yml index 7d1d9fff8..52330582e 100644 --- a/.github/workflows/run-backend-bdd-tests.yml +++ b/.github/workflows/run-backend-bdd-tests.yml @@ -51,9 +51,6 @@ jobs: echo "ACME_DEVELOPMENT_MODE=true" >> .env echo "ACME_FEATURE_ENABLED=true" >> .env echo "ACME_DEVELOPMENT_HTTP01_CHALLENGE_HOST_OVERRIDES={\"localhost\": \"host.docker.internal:8087\"}" >> .env - # XXX: This is a workaround for the error: "error:0308010C:digital envelope routines::unsupported" - # ref: https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported/69699772#69699772 - echo "NODE_OPTIONS=--openssl-legacy-provider" >> .env - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: From 5ecbeb0232a12012a253754eaa3a7cdfbbc5b2d8 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Mon, 10 Nov 2025 16:06:06 -0800 Subject: [PATCH 80/80] lint --- backend/src/ee/services/pki-acme/pki-acme-errors.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/ee/services/pki-acme/pki-acme-errors.ts b/backend/src/ee/services/pki-acme/pki-acme-errors.ts index ba3c9fbc0..837dec8be 100644 --- a/backend/src/ee/services/pki-acme/pki-acme-errors.ts +++ b/backend/src/ee/services/pki-acme/pki-acme-errors.ts @@ -42,6 +42,7 @@ export interface IAcmeError { export class AcmeError extends Error implements IAcmeError { type: AcmeErrorType; + message: string; status: number;