d247db3569
Nova Slides Render / render (push) Failing after 24s
---ci--- project: acdl phase: 1 milestone: v1.29 status: complete ---/ci---
145 lines
5.3 KiB
Python
145 lines
5.3 KiB
Python
"""CAP-037 KMS round-trip test (REQ-350).
|
|
|
|
Sign a test JWT via ``core.kms_signing.sign_jwt()`` (mock KMS with a
|
|
test keypair) → fetch JWKS via ``nova_idp_jwks.lambda_handler()`` (mock
|
|
KMS) → verify the JWT with ``pyjwt`` using the JWKS key. Round-trip
|
|
succeeds — proves the DER→raw conversion + JWK export are mutually
|
|
consistent (the #1 gotcha from RESEARCH §5).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
os.environ.setdefault("NOVA_LAMBDA_LOCAL_BYPASS", "1")
|
|
|
|
_JWKS_PATH = Path(__file__).resolve().parent.parent / "core" / "lambda" / "nova_idp_jwks.py"
|
|
_spec = importlib.util.spec_from_file_location("nova_idp_jwks_rt", _JWKS_PATH)
|
|
jwks_mod = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(jwks_mod)
|
|
|
|
import core.kms_signing as kms_signing
|
|
import jwt as pyjwt
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
|
|
|
|
class _MockKms:
|
|
def __init__(self, priv, pub_der):
|
|
self._priv = priv
|
|
self._pub_der = pub_der
|
|
|
|
def sign(self, KeyId, Message, MessageType, SigningAlgorithm):
|
|
return {"Signature": self._priv.sign(Message, ec.ECDSA(hashes.SHA256()))}
|
|
|
|
def get_public_key(self, KeyId):
|
|
return {"PublicKey": self._pub_der}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset():
|
|
yield
|
|
kms_signing.set_kms_client_for_testing(None)
|
|
|
|
|
|
def test_cap037_kms_roundtrip():
|
|
"""Sign JWT → JWKS → pyjwt verify. The full KMS round-trip (REQ-350)."""
|
|
priv = ec.generate_private_key(ec.SECP256R1())
|
|
pub = priv.public_key()
|
|
pub_der = pub.public_bytes(
|
|
encoding=serialization.Encoding.DER,
|
|
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
)
|
|
kms_signing.set_kms_client_for_testing(_MockKms(priv, pub_der))
|
|
|
|
# 1. Sign a JWT via kms_signing.sign_jwt (uses DER→raw conversion).
|
|
claims = {
|
|
"sub": "roundtrip-user", "aud": "nova-cli", "iss": "nova-idp",
|
|
"exp": 9999999999, "iat": 1700000000, "jti": "rt-jti",
|
|
"roles": ["developer"], "typ": "nova_oidc_token",
|
|
}
|
|
token = kms_signing.sign_jwt(claims, key_id="alias/nova-oidc-signing")
|
|
|
|
# 2. Fetch the JWKS via the JWKS Lambda (mock KMS get_public_key).
|
|
resp = jwks_mod.lambda_handler({}, None)
|
|
assert resp["statusCode"] == 200
|
|
jwks_body = json.loads(resp["body"])
|
|
jwk = jwks_body["keys"][0]
|
|
assert jwk["kty"] == "EC" and jwk["crv"] == "P-256"
|
|
|
|
# 3. Verify the JWT with pyjwt using the JWKS key.
|
|
key = pyjwt.PyJWK(jwk).key
|
|
decoded = pyjwt.decode(token, key, algorithms=["ES256"], audience="nova-cli")
|
|
assert decoded["sub"] == "roundtrip-user"
|
|
assert decoded["jti"] == "rt-jti"
|
|
assert decoded["roles"] == ["developer"]
|
|
assert decoded["typ"] == "nova_oidc_token"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Live-KMS round-trip (REQ-362, Edge 5 item 6).
|
|
#
|
|
# This test is marked ``@pytest.mark.live_aws`` and is SKIPPED in acdl CI
|
|
# (the live KMS key ``alias/nova-oidc-signing`` is not provisioned here).
|
|
# It runs in nova-platform-ops CI against the real KMS key, REQ-362
|
|
# (covered-reference — verification surface is the nova-platform-ops
|
|
# pipeline, not acdl's). It exercises the same sign → JWKS → verify path
|
|
# against the production key/alias so the DER→raw conversion + JWK export
|
|
# are verified end-to-end against real AWS KMS.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _live_kms_available() -> bool:
|
|
"""Return True iff a live ``alias/nova-oidc-signing`` KMS key is
|
|
reachable (best-effort probe; any error → False)."""
|
|
try:
|
|
import boto3
|
|
client = boto3.client("kms")
|
|
client.describe_key(KeyId="alias/nova-oidc-signing")
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@pytest.mark.live_aws
|
|
def test_cap037_kms_roundtrip_live():
|
|
"""Sign → JWKS → pyjwt verify against the LIVE KMS key
|
|
(``alias/nova-oidc-signing``). Edge 5 item 6, REQ-362.
|
|
|
|
Skipped unless a live KMS key is reachable (acdl CI has none; this
|
|
runs in nova-platform-ops CI). The mock-based ``test_cap037_kms_roundtrip``
|
|
above is the acdl-CI-runnable covered-path.
|
|
"""
|
|
if not _live_kms_available():
|
|
pytest.skip(
|
|
"live KMS key alias/nova-oidc-signing not reachable "
|
|
"(acdl CI; runs in nova-platform-ops CI, REQ-362)"
|
|
)
|
|
# Use the real KMS client (reset any test-injected mock client).
|
|
kms_signing.set_kms_client_for_testing(None)
|
|
|
|
claims = {
|
|
"sub": "live-roundtrip-user", "aud": "nova-cli", "iss": "nova-idp",
|
|
"exp": 9999999999, "iat": 1700000000, "jti": "live-rt-jti",
|
|
"roles": ["developer"], "typ": "nova_oidc_token",
|
|
}
|
|
token = kms_signing.sign_jwt(claims, key_id="alias/nova-oidc-signing")
|
|
|
|
resp = jwks_mod.lambda_handler({}, None)
|
|
assert resp["statusCode"] == 200, resp
|
|
jwk = json.loads(resp["body"])["keys"][0]
|
|
assert jwk["kty"] == "EC" and jwk["crv"] == "P-256"
|
|
|
|
key = pyjwt.PyJWK(jwk).key
|
|
decoded = pyjwt.decode(token, key, algorithms=["ES256"], audience="nova-cli")
|
|
assert decoded["sub"] == "live-roundtrip-user"
|
|
assert decoded["jti"] == "live-rt-jti"
|
|
assert decoded["typ"] == "nova_oidc_token" |