diff --git a/backend/bdd/features/pki/acme/challenge.feature b/backend/bdd/features/pki/acme/challenge.feature index bee46c3fb..c747d5d06 100644 --- a/backend/bdd/features/pki/acme/challenge.feature +++ b/backend/bdd/features/pki/acme/challenge.feature @@ -14,8 +14,82 @@ Feature: Challenge 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 select challenge with type http-01 for domain localhost from order in 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" + + Scenario: Did not finish all challenges + 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 + When I create certificate signing request as csr + Then I add names to certificate signing request csr + """ + { + "COMMON_NAME": "localhost" + } + """ + And I add subject alternative name to certificate signing request csr + """ + [ + "infisical.com" + ] + """ + 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 in order as challenge + And I serve challenge response for challenge at localhost + And I tell ACME server that challenge is ready to be verified + + # the localhost auth should be valid + And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "localhost")) | first | .uri" as localhost_auth + And I peak and memorize the next nonce as nonce + When I send a raw ACME request to "{localhost_auth}" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{localhost_auth}", + "kid": "{acme_account.uri}" + } + } + """ + Then the value response.status_code should be equal to 200 + And the value response with jq ".status" should be equal to "valid" + + # the infisical.com auth should still be pending + And I memorize order with jq ".authorizations | map(select(.body.identifier.value == "infisical.com")) | first | .uri" as infisical_auth + And I memorize response.headers with jq ".["replay-nonce"]" as nonce + When I send a raw ACME request to "{infisical_auth}" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{infisical_auth}", + "kid": "{acme_account.uri}" + } + } + """ + Then the value response.status_code should be equal to 200 + And the value response with jq ".status" should be equal to "pending" + + # the order should be pending as well + And I memorize response.headers with jq ".["replay-nonce"]" as nonce + When I send a raw ACME request to "{order.uri}" + """ + { + "protected": { + "alg": "RS256", + "nonce": "{nonce}", + "url": "{order.uri}", + "kid": "{acme_account.uri}" + } + } + """ + Then the value response.status_code should be equal to 200 + And the value response with jq ".status" should be equal to "pending" diff --git a/backend/bdd/features/steps/pki_acme.py b/backend/bdd/features/steps/pki_acme.py index d9004e0ba..d05472ba2 100644 --- a/backend/bdd/features/steps/pki_acme.py +++ b/backend/bdd/features/steps/pki_acme.py @@ -8,6 +8,7 @@ import acme.client import httpx import jq import requests +import requests.structures import glom from faker import Faker from acme import client @@ -115,6 +116,8 @@ def eval_var(context: Context, var_path: str, as_json: bool = True): value = value.to_json() elif isinstance(value, requests.Response): value = value.json() + elif isinstance(value, requests.structures.CaseInsensitiveDict): + value = dict(value.lower_items()) elif isinstance(value, httpx.Response): value = value.json() return value @@ -569,51 +572,58 @@ def step_impl(context: Context, var_path: str): print(json.dumps(value.json(), indent=2)) -@then( - "I select challenge with type {challenge_type} for domain {domain} from order at {var_path} as {challenge_var}" -) -def step_impl( +def select_challenge( context: Context, challenge_type: str, + order_var_path: str, domain: str, - var_path: str, - challenge_var: str, ): - order = eval_var(context, var_path, as_json=False) + acme_client = context.acme_client + order = eval_var(context, order_var_path, as_json=False) + if isinstance(order, dict): + order_body = messages.Order.from_json(order) + order = messages.OrderResource( + body=order_body, + authorizations=[ + acme_client._authzr_from_response( + acme_client._post_as_get(url), uri=url + ) + for url in order_body.authorizations + ], + ) if not isinstance(order, messages.OrderResource): raise ValueError( - f"Expected OrderResource but got {type(order)!r} at {var_path!r}" + f"Expected OrderResource but got {type(order)!r} at {order_var_path!r}" ) auths = list( filter(lambda o: o.body.identifier.value == domain, order.authorizations) ) if not auths: raise ValueError( - f"Authorization for domain {domain!r} not found in {var_path!r}" + f"Authorization for domain {domain!r} not found in {order_var_path!r}" ) if len(auths) > 1: raise ValueError( - f"More than one order for domain {domain!r} found in {var_path!r}" + f"More than one order for domain {domain!r} found in {order_var_path!r}" ) auth = auths[0] challenges = list(filter(lambda a: a.typ == challenge_type, auth.body.challenges)) if not challenges: raise ValueError( - f"Authorization type {challenge_type!r} not found in {var_path!r}" + f"Authorization type {challenge_type!r} not found in {order_var_path!r}" ) if len(challenges) > 1: raise ValueError( - f"More than one authorization for type {challenge_type!r} found in {var_path!r}" + f"More than one authorization for type {challenge_type!r} found in {order_var_path!r}" ) - context.vars[challenge_var] = challenges[0] + return challenges[0] -@then("I serve challenge response for {var_path} at {hostname}") -def step_impl(context: Context, var_path: str, hostname: str): - if hostname != "localhost": - raise ValueError("Currently only localhost is supported") - challenge = eval_var(context, var_path, as_json=False) +def serve_challenge( + context: Context, + challenge: messages.ChallengeBody, +): response, validation = challenge.response_and_validation( context.acme_client.net.key ) @@ -629,14 +639,43 @@ def step_impl(context: Context, var_path: str, hostname: str): context.web_server = web_server -@then("I tell ACME server that {var_path} is ready to be verified") -def step_impl(context: Context, var_path: str): - challenge = eval_var(context, var_path, as_json=False) +def notify_challenge_ready(context: Context, challenge: messages.ChallengeBody): acme_client = context.acme_client response, validation = challenge.response_and_validation(acme_client.net.key) acme_client.answer_challenge(challenge, response) +@then( + "I select challenge with type {challenge_type} for domain {domain} from order in {var_path} as {challenge_var}" +) +def step_impl( + context: Context, + challenge_type: str, + domain: str, + var_path: str, + challenge_var: str, +): + challenge = select_challenge( + context=context, + challenge_type=challenge_type, + domain=domain, + order_var_path=var_path, + ) + context.vars[challenge_var] = challenge + + +@then("I serve challenge response for {var_path} at {hostname}") +def step_impl(context: Context, var_path: str, hostname: str): + challenge = eval_var(context, var_path, as_json=False) + serve_challenge(context=context, challenge=challenge) + + +@then("I tell ACME server that {var_path} is ready to be verified") +def step_impl(context: Context, var_path: str): + challenge = eval_var(context, var_path, as_json=False) + notify_challenge_ready(context=context, challenge=challenge) + + @then("I poll and finalize the ACME order {var_path} as {finalized_var}") def step_impl(context: Context, var_path: str, finalized_var: str): order = eval_var(context, var_path, as_json=False)