Finish infra for sending raw ACME req, add tests for nonce

This commit is contained in:
Fang-Pen Lin
2025-11-06 17:23:17 -08:00
parent 44fc551076
commit 2a4b122348
6 changed files with 71 additions and 9 deletions

View File

@@ -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/(.+)

View File

@@ -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

View File

@@ -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

View File

@@ -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
"""

View File

@@ -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>[^/]+)$") | .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"

View File

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