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}" )