mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-07 22:53:55 -05:00
891 lines
30 KiB
Python
891 lines
30 KiB
Python
import json
|
|
import logging
|
|
import re
|
|
import urllib.parse
|
|
|
|
import acme.client
|
|
import jq
|
|
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 json_util
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography import x509
|
|
from cryptography.x509.oid import NameOID
|
|
from cryptography.hazmat.primitives import hashes
|
|
|
|
from features.steps.utils import define_nock, clean_all_nock, restore_nock
|
|
from utils import replace_vars, with_nocks
|
|
from utils import eval_var
|
|
from utils import prepare_headers
|
|
|
|
|
|
ACC_KEY_BITS = 2048
|
|
ACC_KEY_PUBLIC_EXPONENT = 65537
|
|
logger = logging.getLogger(__name__)
|
|
faker = Faker()
|
|
|
|
|
|
class AcmeProfile:
|
|
def __init__(self, id: str, eab_kid: str, eab_secret: str):
|
|
self.id = id
|
|
self.eab_kid = eab_kid
|
|
self.eab_secret = eab_secret
|
|
|
|
|
|
@given("I make a random {faker_type} as {var_name}")
|
|
def step_impl(context: Context, faker_type: str, var_name: str):
|
|
context.vars[var_name] = getattr(faker, faker_type)()
|
|
|
|
|
|
@given('I have an ACME cert profile as "{profile_var}"')
|
|
def step_impl(context: Context, profile_var: str):
|
|
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:
|
|
profile_slug = faker.slug()
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
response = context.http_client.post(
|
|
"/api/v1/pki/certificate-profiles",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
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=dict(authorization="Bearer {}".format(jwt_token)),
|
|
)
|
|
response.raise_for_status()
|
|
resp_json = response.json()
|
|
secret = resp_json["eabSecret"]
|
|
|
|
context.vars[profile_var] = AcmeProfile(
|
|
profile_id,
|
|
eab_kid=kid,
|
|
eab_secret=secret,
|
|
)
|
|
|
|
|
|
@given("I create a Cloudflare connection as {var_name}")
|
|
def step_impl(context: Context, var_name: str):
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
conn_slug = faker.slug()
|
|
mock_account_id = "MOCK_ACCOUNT_ID"
|
|
with with_nocks(
|
|
context,
|
|
definitions=[
|
|
{
|
|
"scope": "https://api.cloudflare.com:443",
|
|
"method": "GET",
|
|
"path": f"/client/v4/accounts/{mock_account_id}",
|
|
"status": 200,
|
|
"response": {
|
|
"result": {
|
|
"id": "A2A6347F-88B5-442D-9798-95E408BC7701",
|
|
"name": "Mock Account",
|
|
"type": "standard",
|
|
"settings": {
|
|
"enforce_twofactor": True,
|
|
"api_access_enabled": None,
|
|
"access_approval_expiry": None,
|
|
"abuse_contact_email": None,
|
|
"user_groups_ui_beta": False,
|
|
},
|
|
"legacy_flags": {
|
|
"enterprise_zone_quota": {
|
|
"maximum": 0,
|
|
"current": 0,
|
|
"available": 0,
|
|
}
|
|
},
|
|
"created_on": "2013-04-18T00:41:02.215243Z",
|
|
},
|
|
"success": True,
|
|
"errors": [],
|
|
"messages": [],
|
|
},
|
|
"responseIsBinary": False,
|
|
}
|
|
],
|
|
):
|
|
response = context.http_client.post(
|
|
"/api/v1/app-connections/cloudflare",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
json={
|
|
"name": conn_slug,
|
|
"description": "",
|
|
"method": "api-token",
|
|
"credentials": {
|
|
"apiToken": "MOCK_API_TOKEN",
|
|
"accountId": mock_account_id,
|
|
},
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
context.vars[var_name] = response
|
|
|
|
|
|
@given("I create a external ACME CA with the following config as {var_name}")
|
|
def step_impl(context: Context, var_name: str):
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
ca_slug = faker.slug()
|
|
config = replace_vars(json.loads(context.text), context.vars)
|
|
response = context.http_client.post(
|
|
"/api/v1/pki/ca/acme",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
json={
|
|
"projectId": context.vars["PROJECT_ID"],
|
|
"name": ca_slug,
|
|
"type": "acme",
|
|
"status": "active",
|
|
"enableDirectIssuance": True,
|
|
"configuration": config,
|
|
},
|
|
)
|
|
response.raise_for_status()
|
|
context.vars[var_name] = response
|
|
|
|
|
|
@given("I create a certificate template with the following config as {var_name}")
|
|
def step_impl(context: Context, var_name: str):
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
template_slug = faker.slug()
|
|
config = replace_vars(json.loads(context.text), context.vars)
|
|
response = context.http_client.post(
|
|
"/api/v2/certificate-templates",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
json={
|
|
"projectId": context.vars["PROJECT_ID"],
|
|
"name": template_slug,
|
|
"description": "",
|
|
}
|
|
| config,
|
|
)
|
|
response.raise_for_status()
|
|
context.vars[var_name] = response
|
|
|
|
|
|
@given(
|
|
'I create an ACME profile with ca {ca_id} and template {template_id} as "{profile_var}"'
|
|
)
|
|
def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str):
|
|
profile_slug = faker.slug()
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
response = context.http_client.post(
|
|
"/api/v1/pki/certificate-profiles",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
json={
|
|
"projectId": context.vars["PROJECT_ID"],
|
|
"slug": profile_slug,
|
|
"description": "ACME Profile created by BDD test",
|
|
"enrollmentType": "acme",
|
|
"caId": replace_vars(ca_id, context.vars),
|
|
"certificateTemplateId": replace_vars(template_id, context.vars),
|
|
"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=dict(authorization="Bearer {}".format(jwt_token)),
|
|
)
|
|
response.raise_for_status()
|
|
resp_json = response.json()
|
|
secret = resp_json["eabSecret"]
|
|
|
|
context.vars[profile_var] = AcmeProfile(
|
|
profile_id,
|
|
eab_kid=kid,
|
|
eab_secret=secret,
|
|
)
|
|
|
|
|
|
@given('I have an ACME cert profile with external ACME CA as "{profile_var}"')
|
|
def step_impl(context: Context, profile_var: str):
|
|
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:
|
|
profile_slug = faker.slug()
|
|
jwt_token = context.vars["AUTH_TOKEN"]
|
|
response = context.http_client.post(
|
|
"/api/v1/pki/certificate-profiles",
|
|
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
|
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=dict(authorization="Bearer {}".format(jwt_token)),
|
|
)
|
|
response.raise_for_status()
|
|
resp_json = response.json()
|
|
secret = resp_json["eabSecret"]
|
|
|
|
context.vars[profile_var] = AcmeProfile(
|
|
profile_id,
|
|
eab_kid=kid,
|
|
eab_secret=secret,
|
|
)
|
|
|
|
|
|
@given("I intercept outgoing requests")
|
|
def step_impl(context: Context):
|
|
definitions = replace_vars(json.loads(context.text), context.vars)
|
|
define_nock(context, definitions)
|
|
|
|
|
|
@then("I reset requests interceptions")
|
|
def step_impl(context: Context):
|
|
clean_all_nock(context)
|
|
restore_nock(context)
|
|
|
|
|
|
@given("I use {token_var} for authentication")
|
|
def step_impl(context: Context, token_var: str):
|
|
context.auth_token = eval_var(context, token_var)
|
|
|
|
|
|
@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(
|
|
method, url.format(**context.vars), headers=prepare_headers(context)
|
|
)
|
|
context.vars["response"] = response
|
|
logger.debug("Response status: %r", response.status_code)
|
|
try:
|
|
logger.debug("Response JSON payload: %r", response.json())
|
|
except json.decoder.JSONDecodeError:
|
|
pass
|
|
|
|
|
|
@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)
|
|
logger.debug(
|
|
"Sending %s request to %s with JSON payload: %s",
|
|
method,
|
|
url,
|
|
json.dumps(json_payload),
|
|
)
|
|
response = context.http_client.request(
|
|
method,
|
|
url.format(**context.vars),
|
|
headers=prepare_headers(context),
|
|
json=json_payload,
|
|
)
|
|
context.vars["response"] = response
|
|
logger.debug("Response status: %r", response.status_code)
|
|
logger.debug("Response JSON payload: %r", response.json())
|
|
|
|
|
|
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, (
|
|
f"{context.vars['response'].status_code} != {expected_status_code}"
|
|
)
|
|
|
|
|
|
@then('the response header "{header}" should contains non-empty value')
|
|
def step_impl(context: Context, header: str):
|
|
header_value = context.vars["response"].headers.get(header)
|
|
assert header_value is not None, f"Header {header} not found in response"
|
|
assert header_value, (
|
|
f"Header {header} found in response, but value {header_value:!r} is empty"
|
|
)
|
|
|
|
|
|
@then("the response body should match JSON value")
|
|
def step_impl(context: Context):
|
|
payload = context.vars["response"].json()
|
|
expected = json.loads(context.text)
|
|
replaced = replace_vars(expected, context.vars)
|
|
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)
|
|
|
|
|
|
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 not only_return_existing:
|
|
# clear the account in case if we want to register twice
|
|
acme_client.net.account = None
|
|
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=eab_directory,
|
|
hmac_alg="HS256",
|
|
)
|
|
registration = messages.NewRegistration.from_data(
|
|
email=email,
|
|
external_account_binding=eab,
|
|
only_return_existing=only_return_existing,
|
|
)
|
|
try:
|
|
if not only_return_existing:
|
|
context.vars[account_var] = acme_client.new_account(registration)
|
|
else:
|
|
context.vars[account_var] = acme_client.query_registration(
|
|
acme_client.net.account
|
|
)
|
|
except Exception as exp:
|
|
logger.error(f"Failed to register: {exp}", exc_info=True)
|
|
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 find the existing ACME account without EAB as {account_var}")
|
|
def step_impl(context: Context, account_var: str):
|
|
acme_client = context.acme_client
|
|
# registration = messages.RegistrationResource.from_json(dict(uri=""))
|
|
registration = acme_client.net.account
|
|
try:
|
|
context.vars[account_var] = acme_client.query_registration(registration)
|
|
except Exception as exp:
|
|
context.vars["error"] = exp
|
|
|
|
|
|
@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:
|
|
context.vars["error"] = 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)
|
|
protected = replace_vars(content["protected"], context.vars)
|
|
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:
|
|
encoded_payload = 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"]
|
|
actual_url = urllib.parse.urljoin(base_url, replace_vars(url, context.vars))
|
|
response = acme_client.net._send_request(
|
|
"POST",
|
|
actual_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 encode CSR {pem_var} as JOSE Base-64 DER as {var_name}",
|
|
)
|
|
def step_impl(context: Context, pem_var: str, var_name: str):
|
|
csr = eval_var(context, pem_var)
|
|
parsed_csr = x509.load_pem_x509_csr(csr)
|
|
context.vars[var_name] = json_util.encode_csr(parsed_csr)
|
|
|
|
|
|
@then(
|
|
"I submit the certificate signing request PEM {pem_var} certificate order to the ACME server as {order_var}"
|
|
)
|
|
def step_impl(context: Context, pem_var: str, order_var: str):
|
|
context.vars[order_var] = context.acme_client.new_order(context.vars[pem_var])
|
|
|
|
|
|
@then("I send an ACME post-as-get to {uri_path} as {res_var}")
|
|
def step_impl(context: Context, uri_path: str, res_var: str):
|
|
uri_value = eval_var(context, uri_path)
|
|
context.vars[res_var] = context.acme_client._post_as_get(uri_value)
|
|
|
|
|
|
@when("I create certificate signing request as {csr_var}")
|
|
def step_impl(context: Context, csr_var: str):
|
|
context.vars[csr_var] = x509.CertificateSigningRequestBuilder()
|
|
|
|
|
|
@then("I add names to certificate signing request {csr_var}")
|
|
def step_impl(context: Context, csr_var: str):
|
|
names = json.loads(context.text)
|
|
builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var]
|
|
context.vars[csr_var] = builder.subject_name(
|
|
x509.Name(
|
|
[
|
|
x509.NameAttribute(getattr(NameOID, name), value)
|
|
for name, value in names.items()
|
|
]
|
|
)
|
|
)
|
|
|
|
|
|
@then("I add subject alternative name to certificate signing request {csr_var}")
|
|
def step_impl(context: Context, csr_var: str):
|
|
names = json.loads(context.text)
|
|
builder: x509.CertificateSigningRequestBuilder = context.vars[csr_var]
|
|
context.vars[csr_var] = builder.add_extension(
|
|
x509.SubjectAlternativeName([x509.DNSName(name) for name in names]),
|
|
critical=False,
|
|
)
|
|
|
|
|
|
@then("I create a RSA private key pair as {rsa_key_var}")
|
|
def step_impl(context: Context, rsa_key_var: str):
|
|
context.vars[rsa_key_var] = rsa.generate_private_key(
|
|
# TODO: make them configurable if we need to
|
|
public_exponent=65537,
|
|
key_size=2048,
|
|
)
|
|
|
|
|
|
@then(
|
|
"I sign the certificate signing request {csr_var} with private key {pk_var} and output it as {pem_var} in PEM format"
|
|
)
|
|
def step_impl(context: Context, csr_var: str, pk_var: str, pem_var: str):
|
|
context.vars[pem_var] = (
|
|
context.vars[csr_var]
|
|
.sign(context.vars[pk_var], hashes.SHA256())
|
|
.public_bytes(serialization.Encoding.PEM)
|
|
)
|
|
|
|
|
|
@then("the value {var_path} should be true for jq {query}")
|
|
def step_impl(context: Context, var_path: str, query: str):
|
|
value = eval_var(context, var_path)
|
|
result = jq.compile(replace_vars(query, context.vars)).input_value(value).first()
|
|
assert result, f"{value} does not match {query}"
|
|
|
|
|
|
def apply_value_with_jq(context: Context, var_path: str, jq_query: str):
|
|
value = eval_var(context, var_path)
|
|
return value, jq.compile(replace_vars(jq_query, context.vars)).input_value(
|
|
value
|
|
).first()
|
|
|
|
|
|
@then('the value {var_path} with jq "{jq_query}" should be equal to json')
|
|
def step_impl(context: Context, var_path: str, jq_query: str):
|
|
value, result = apply_value_with_jq(
|
|
context=context,
|
|
var_path=var_path,
|
|
jq_query=jq_query,
|
|
)
|
|
expected_value = json.loads(context.text)
|
|
assert result == expected_value, (
|
|
f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}"
|
|
)
|
|
|
|
|
|
@then('the value {var_path} with jq "{jq_query}" should be present')
|
|
def step_impl(context: Context, var_path: str, jq_query: str):
|
|
value, result = apply_value_with_jq(
|
|
context=context,
|
|
var_path=var_path,
|
|
jq_query=jq_query,
|
|
)
|
|
assert result, (
|
|
f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} is not present"
|
|
)
|
|
|
|
|
|
@then("the value {var_path} with should be absent")
|
|
def step_impl(context: Context, var_path: str):
|
|
try:
|
|
value = eval_var(context, var_path)
|
|
except Exception as exp:
|
|
if isinstance(exp, KeyError):
|
|
return
|
|
raise
|
|
assert False, (
|
|
f"value at {var_path!r} should be absent, but we got this instead: {value!r}"
|
|
)
|
|
|
|
|
|
@then('the value {var_path} with jq "{jq_query}" should be equal to {expected}')
|
|
def step_impl(context: Context, var_path: str, jq_query: str, expected: str):
|
|
value, result = apply_value_with_jq(
|
|
context=context,
|
|
var_path=var_path,
|
|
jq_query=jq_query,
|
|
)
|
|
expected_value = replace_vars(json.loads(expected), context.vars)
|
|
assert result == expected_value, (
|
|
f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {json.dumps(expected_value)!r}"
|
|
)
|
|
|
|
|
|
@then('the value {var_path} with jq "{jq_query}" should match pattern {regex}')
|
|
def step_impl(context: Context, var_path: str, jq_query: str, regex: str):
|
|
actual_regex = replace_vars(regex, context.vars)
|
|
value, result = apply_value_with_jq(
|
|
context=context,
|
|
var_path=var_path,
|
|
jq_query=jq_query,
|
|
)
|
|
assert re.match(actual_regex, result), (
|
|
f"{json.dumps(value)!r} with jq {jq_query!r}, the result {json.dumps(result)!r} does not match {actual_regex!r}"
|
|
)
|
|
|
|
|
|
@then("the value {var_path} should be equal to json")
|
|
def step_impl(context: Context, var_path: str):
|
|
value = eval_var(context, var_path)
|
|
expected_value = json.loads(context.text)
|
|
assert value == expected_value, f"{value!r} does not match {expected_value!r}"
|
|
|
|
|
|
@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 = replace_vars(json.loads(expected), context.vars)
|
|
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(
|
|
context=context,
|
|
var_path=var_path,
|
|
jq_query=jq_query,
|
|
)
|
|
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 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)
|
|
context.vars[var_name] = value
|
|
|
|
|
|
@then("I print the value {var_path}")
|
|
def step_impl(context: Context, var_path: str):
|
|
value = eval_var(context, var_path)
|
|
print(json.dumps(value.json(), indent=2))
|
|
|
|
|
|
def select_challenge(
|
|
context: Context,
|
|
challenge_type: str,
|
|
order_var_path: str,
|
|
domain: str,
|
|
):
|
|
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 {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 {order_var_path!r}"
|
|
)
|
|
if len(auths) > 1:
|
|
raise ValueError(
|
|
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 {order_var_path!r}"
|
|
)
|
|
if len(challenges) > 1:
|
|
raise ValueError(
|
|
f"More than one authorization for type {challenge_type!r} found in {order_var_path!r}"
|
|
)
|
|
return challenges[0]
|
|
|
|
|
|
def serve_challenge(
|
|
context: Context,
|
|
challenge: messages.ChallengeBody,
|
|
):
|
|
if hasattr(context, "web_server"):
|
|
context.web_server.shutdown_and_server_close()
|
|
|
|
response, validation = challenge.response_and_validation(
|
|
context.acme_client.net.key
|
|
)
|
|
resource = standalone.HTTP01RequestHandler.HTTP01Resource(
|
|
chall=challenge.chall, response=response, validation=validation
|
|
)
|
|
# TODO: make port configurable
|
|
servers = standalone.HTTP01DualNetworkedServers(("0.0.0.0", 8087), {resource})
|
|
servers.serve_forever()
|
|
context.web_server = servers
|
|
|
|
|
|
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 pass all challenges with type {challenge_type} for order in {order_var_path}")
|
|
def step_impl(
|
|
context: Context,
|
|
challenge_type: str,
|
|
order_var_path: str,
|
|
):
|
|
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 {order_var_path!r}"
|
|
)
|
|
|
|
for domain in order.body.identifiers:
|
|
logger.info(
|
|
"Selecting challenge for domain %s with type %s ...",
|
|
domain.value,
|
|
challenge_type,
|
|
)
|
|
challenge = select_challenge(
|
|
context=context,
|
|
challenge_type=challenge_type,
|
|
domain=domain.value,
|
|
order_var_path=order_var_path,
|
|
)
|
|
logger.info(
|
|
"Found challenge for domain %s with type %s, challenge=%s",
|
|
domain.value,
|
|
challenge_type,
|
|
challenge.uri,
|
|
)
|
|
|
|
logger.info(
|
|
"Serving challenge for domain %s with type %s ...",
|
|
domain.value,
|
|
challenge_type,
|
|
)
|
|
serve_challenge(context=context, challenge=challenge)
|
|
|
|
logger.info(
|
|
"Notifying challenge for domain %s with type %s ...", domain, challenge_type
|
|
)
|
|
notify_challenge_ready(context=context, challenge=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)
|
|
acme_client = context.acme_client
|
|
finalized_order = acme_client.poll_and_finalize(order)
|
|
context.vars[finalized_var] = finalized_order
|
|
|
|
|
|
@then("I parse the full-chain certificate from order {order_var_path} as {cert_var}")
|
|
def step_impl(context: Context, order_var_path: str, cert_var: str):
|
|
order = eval_var(context, order_var_path, as_json=False)
|
|
cert = x509.load_pem_x509_certificate(order.fullchain_pem.encode())
|
|
context.vars[cert_var] = cert
|