Skip to content

Commit d8b6cef

Browse files
committed
added more tests
1 parent 7f1a345 commit d8b6cef

3 files changed

Lines changed: 149 additions & 4 deletions

File tree

tests/e2e/features/steps/tls.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,46 @@ def configure_mtls_wrong_client_cert(context: Context) -> None:
210210
_write_config(config, _LLAMA_STACK_CONFIG)
211211

212212

213+
@given("Llama Stack is configured for mTLS with untrusted client certificate")
214+
def configure_mtls_untrusted_client_cert(context: Context) -> None:
215+
"""Configure run.yaml for mTLS with a client certificate not trusted by the server's CA.
216+
217+
Uses a client certificate issued by a separate, untrusted CA, causing the
218+
mTLS handshake to fail.
219+
220+
Parameters:
221+
context: Behave test context.
222+
"""
223+
config, provider = _prepare_tls_provider()
224+
provider["config"]["base_url"] = "https://mock-tls-inference:8444/v1"
225+
provider["config"]["network"]["tls"] = {
226+
"verify": "/certs/ca.crt",
227+
"client_cert": "/certs/untrusted-client.crt",
228+
"client_key": "/certs/untrusted-client.key",
229+
}
230+
_write_config(config, _LLAMA_STACK_CONFIG)
231+
232+
233+
@given("Llama Stack is configured for mTLS with expired client certificate")
234+
def configure_mtls_expired_client_cert(context: Context) -> None:
235+
"""Configure run.yaml for mTLS with an expired client certificate.
236+
237+
Uses a client certificate that was signed by the correct CA but has
238+
expired validity dates.
239+
240+
Parameters:
241+
context: Behave test context.
242+
"""
243+
config, provider = _prepare_tls_provider()
244+
provider["config"]["base_url"] = "https://mock-tls-inference:8444/v1"
245+
provider["config"]["network"]["tls"] = {
246+
"verify": "/certs/ca.crt",
247+
"client_cert": "/certs/expired-client.crt",
248+
"client_key": "/certs/client.key",
249+
}
250+
_write_config(config, _LLAMA_STACK_CONFIG)
251+
252+
213253
@given('Llama Stack is configured with TLS minimum version "{version}"')
214254
def configure_tls_min_version(context: Context, version: str) -> None:
215255
"""Configure run.yaml with TLS minimum version.
@@ -224,3 +264,22 @@ def configure_tls_min_version(context: Context, version: str) -> None:
224264
"min_version": version,
225265
}
226266
_write_config(config, _LLAMA_STACK_CONFIG)
267+
268+
269+
@given('Llama Stack is configured with TLS minimum version "{version}" and CA certificate path "{path}"')
270+
def configure_tls_min_version_with_ca_path(
271+
context: Context, version: str, path: str
272+
) -> None:
273+
"""Configure run.yaml with TLS minimum version and a specific CA cert path.
274+
275+
Parameters:
276+
context: Behave test context.
277+
version: The TLS version (e.g., "TLSv1.2", "TLSv1.3").
278+
path: Path to the CA certificate file.
279+
"""
280+
config, provider = _prepare_tls_provider()
281+
provider["config"]["network"]["tls"] = {
282+
"verify": path,
283+
"min_version": version,
284+
}
285+
_write_config(config, _LLAMA_STACK_CONFIG)

tests/e2e/features/tls.feature

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ Feature: TLS configuration for remote inference providers
9494
Then The status code of the response is 500
9595
And The body of the response does not contain Hello from the TLS mock inference server
9696

97+
Scenario: Inference fails when mTLS is required but untrusted client certificate is provided
98+
Given Llama Stack is configured for mTLS with untrusted client certificate
99+
And Llama Stack is restarted
100+
And Lightspeed Stack is restarted
101+
When I use "query" to ask question
102+
"""
103+
{"query": "Say hello", "model": "mock-tls-model", "provider": "tls-openai"}
104+
"""
105+
Then The status code of the response is 500
106+
And The body of the response does not contain Hello from the TLS mock inference server
107+
108+
Scenario: Inference fails when mTLS is required but expired client certificate is provided
109+
Given Llama Stack is configured for mTLS with expired client certificate
110+
And Llama Stack is restarted
111+
And Lightspeed Stack is restarted
112+
When I use "query" to ask question
113+
"""
114+
{"query": "Say hello", "model": "mock-tls-model", "provider": "tls-openai"}
115+
"""
116+
Then The status code of the response is 500
117+
And The body of the response does not contain Hello from the TLS mock inference server
118+
97119
Scenario: Inference succeeds with TLS minimum version TLSv1.3
98120
Given Llama Stack is configured with TLS minimum version "TLSv1.3"
99121
And Llama Stack is restarted
@@ -103,3 +125,25 @@ Feature: TLS configuration for remote inference providers
103125
{"query": "Say hello", "model": "mock-tls-model", "provider": "tls-openai"}
104126
"""
105127
Then The status code of the response is 200
128+
129+
Scenario: Inference fails with TLS minimum version TLSv1.3 and untrusted CA certificate
130+
Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/untrusted-ca.crt"
131+
And Llama Stack is restarted
132+
And Lightspeed Stack is restarted
133+
When I use "query" to ask question
134+
"""
135+
{"query": "Say hello", "model": "mock-tls-model", "provider": "tls-openai"}
136+
"""
137+
Then The status code of the response is 500
138+
And The body of the response does not contain Hello from the TLS mock inference server
139+
140+
Scenario: Inference fails with TLS minimum version TLSv1.3 and expired CA certificate
141+
Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/expired-ca.crt"
142+
And Llama Stack is restarted
143+
And Lightspeed Stack is restarted
144+
When I use "query" to ask question
145+
"""
146+
{"query": "Say hello", "model": "mock-tls-model", "provider": "tls-openai"}
147+
"""
148+
Then The status code of the response is 500
149+
And The body of the response does not contain Hello from the TLS mock inference server

tests/e2e/mock_tls_inference_server/server.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,34 @@ def _send_json(self, data: dict | list) -> None:
108108
self.wfile.write(payload)
109109

110110

111+
def _export_expired_client_cert(
112+
ca: trustme.CA, client_cert: trustme.LeafCert, path: Path
113+
) -> None:
114+
"""Re-sign a client certificate with expired validity dates.
115+
116+
Parameters:
117+
ca: The CA that issued the original client certificate.
118+
client_cert: The original client leaf certificate.
119+
path: File path to write the expired client certificate PEM.
120+
"""
121+
original = client_cert.cert_chain_pems[0].bytes()
122+
from cryptography.x509 import load_pem_x509_certificate
123+
124+
orig_cert = load_pem_x509_certificate(original)
125+
now = datetime.datetime.now(datetime.UTC)
126+
builder = CertificateBuilder()
127+
builder = builder.subject_name(orig_cert.subject)
128+
builder = builder.issuer_name(orig_cert.issuer)
129+
builder = builder.public_key(orig_cert.public_key())
130+
builder = builder.serial_number(random_serial_number())
131+
builder = builder.not_valid_before(now - datetime.timedelta(days=365))
132+
builder = builder.not_valid_after(now - datetime.timedelta(seconds=1))
133+
for ext in orig_cert.extensions:
134+
builder = builder.add_extension(ext.value, ext.critical)
135+
expired_cert = builder.sign(ca._private_key, hashes.SHA256())
136+
path.write_bytes(expired_cert.public_bytes(serialization.Encoding.PEM))
137+
138+
111139
def _export_expired_ca_cert(ca: trustme.CA, path: Path) -> None:
112140
"""Re-sign a trustme CA certificate with expired validity dates.
113141
@@ -118,18 +146,18 @@ def _export_expired_ca_cert(ca: trustme.CA, path: Path) -> None:
118146
ca: The trustme CA whose certificate and key to use.
119147
path: File path to write the expired CA certificate PEM.
120148
"""
121-
original = ca._certificate # noqa: SLF001
122-
now = datetime.datetime.now(datetime.timezone.utc)
149+
original = ca._certificate
150+
now = datetime.datetime.now(datetime.UTC)
123151
builder = CertificateBuilder()
124152
builder = builder.subject_name(original.subject)
125153
builder = builder.issuer_name(original.issuer)
126154
builder = builder.public_key(original.public_key())
127155
builder = builder.serial_number(random_serial_number())
128156
builder = builder.not_valid_before(now - datetime.timedelta(days=365))
129-
builder = builder.not_valid_after(now - datetime.timedelta(days=1))
157+
builder = builder.not_valid_after(now - datetime.timedelta(seconds=1))
130158
for ext in original.extensions:
131159
builder = builder.add_extension(ext.value, ext.critical)
132-
expired_cert = builder.sign(ca._private_key, hashes.SHA256()) # noqa: SLF001
160+
expired_cert = builder.sign(ca._private_key, hashes.SHA256())
133161
path.write_bytes(expired_cert.public_bytes(serialization.Encoding.PEM))
134162

135163

@@ -211,6 +239,20 @@ def main() -> None:
211239
_export_expired_ca_cert(ca, certs_dir / "expired-ca.crt")
212240
print(f" Expired CA cert: {certs_dir / 'expired-ca.crt'}")
213241

242+
# Export untrusted client certificate (issued by a different CA)
243+
untrusted_client = untrusted_ca.issue_cert("tls-e2e-untrusted-client")
244+
untrusted_client.private_key_pem.write_to_path(
245+
str(certs_dir / "untrusted-client.key")
246+
)
247+
with (certs_dir / "untrusted-client.crt").open("wb") as f:
248+
for blob in untrusted_client.cert_chain_pems:
249+
f.write(blob.bytes())
250+
print(f" Untrusted client cert: {certs_dir / 'untrusted-client.crt'}")
251+
252+
# Export expired client certificate (signed by main CA but with past dates)
253+
_export_expired_client_cert(ca, client_cert, certs_dir / "expired-client.crt")
254+
print(f" Expired client cert: {certs_dir / 'expired-client.crt'}")
255+
214256
print("=" * 60)
215257
print("Starting servers...")
216258
print("=" * 60)

0 commit comments

Comments
 (0)