"""AES-256-GCM backend selection: known answers, interop and tamper detection. Both the optional native backend (`cryptography`) and the stdlib-only pure-Python backend run the same vectors through the public aes_gcm_encrypt/aes_gcm_decrypt API used by node and bundle encryption. """ import importlib.util import pathlib import pytest SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "resources/scripts/napseer_mcp_server.py" h = bytes.fromhex GCM_KEY = h("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308") GCM_IV = h("cafebabefacedbaddecaf888") GCM_P = h( "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72" "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255" ) GCM_C = h( "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa" "8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad" ) # The GCM specification (McGrew & Viega) test cases 13-16, AES-256. KNOWN_ANSWERS = [ ("tc13", bytes(32), bytes(12), b"", b"", b"", h("530f8afbc74536b9a963b4f1c4cb738b")), ("tc14", bytes(32), bytes(12), b"", bytes(16), h("cea7403d4d606b6e074ec5d3baf39d18"), h("d0d1c8a799996bf0265b98b5d48ab919")), ("tc15", GCM_KEY, GCM_IV, b"", GCM_P, GCM_C, h("b094dac5d93471bdec1a502270e3cc6c")), ("tc16", GCM_KEY, GCM_IV, h("feedfacedeadbeeffeedfacedeadbeefabaddad2"), GCM_P[:60], GCM_C[:60], h("76fc6ece0f4e1768cddf8853bb2d551b")), ] @pytest.fixture(scope="module") def mod(): spec = importlib.util.spec_from_file_location("crypto_backend_test_worker", SCRIPT) module = importlib.util.module_from_spec(spec) import sys sys.path.insert(0, str(SCRIPT.parent)) spec.loader.exec_module(module) yield module module.select_crypto_backend() def backends(mod): names = [mod.CRYPTO_BACKEND_PURE] if mod.native_crypto_available(): names.append(mod.CRYPTO_BACKEND_NATIVE) return names def use(mod, name): assert mod.select_crypto_backend(name) == name return name def test_native_backend_is_selected_when_cryptography_is_importable(mod, monkeypatch): pytest.importorskip("cryptography") monkeypatch.delenv("NAPSEER_CRYPTO_BACKEND", raising=False) assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_NATIVE monkeypatch.setenv("NAPSEER_CRYPTO_BACKEND", "pure_python") assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_PURE assert mod.crypto_backend_status()["crypto_backend"] == "pure_python" def test_pure_backend_is_used_when_native_is_unavailable(mod, monkeypatch): monkeypatch.setattr(mod, "_NativeAESGCM", None) monkeypatch.delenv("NAPSEER_CRYPTO_BACKEND", raising=False) try: assert mod.select_crypto_backend() == mod.CRYPTO_BACKEND_PURE status = mod.crypto_backend_status() assert status["native_crypto_available"] is False assert "cryptography" in status["crypto_backend_note"] finally: monkeypatch.undo() mod.select_crypto_backend() @pytest.mark.parametrize("case", KNOWN_ANSWERS, ids=[case[0] for case in KNOWN_ANSWERS]) def test_known_answer_vectors_through_every_backend(mod, case): _name, key, iv, aad, plaintext, ciphertext, tag = case for backend in backends(mod): use(mod, backend) assert mod.aes_gcm_encrypt(key, iv, aad, plaintext) == ciphertext + tag, backend assert mod.aes_gcm_decrypt(key, iv, aad, ciphertext + tag) == plaintext, backend mod.select_crypto_backend() def test_cross_backend_interop(mod): pytest.importorskip("cryptography") key = bytes(range(32)) nonce = bytes(range(12)) aad = b"napseer interop aad" for size in (0, 1, 15, 16, 17, 1000): plaintext = bytes((index * 7) & 0xFF for index in range(size)) use(mod, mod.CRYPTO_BACKEND_PURE) pure = mod.aes_gcm_encrypt(key, nonce, aad, plaintext) use(mod, mod.CRYPTO_BACKEND_NATIVE) native = mod.aes_gcm_encrypt(key, nonce, aad, plaintext) assert pure == native assert mod.aes_gcm_decrypt(key, nonce, aad, pure) == plaintext use(mod, mod.CRYPTO_BACKEND_PURE) assert mod.aes_gcm_decrypt(key, nonce, aad, native) == plaintext mod.select_crypto_backend() def test_tamper_detection_raises_the_same_error_in_every_backend(mod): key = bytes(range(32)) nonce = bytes(12) for backend in backends(mod): use(mod, backend) packed = mod.aes_gcm_encrypt(key, nonce, b"aad", b"synthetic payload") tampered_body = bytes([packed[0] ^ 1]) + packed[1:] tampered_tag = packed[:-1] + bytes([packed[-1] ^ 0x80]) wrong_key = bytes([key[0] ^ 1]) + key[1:] for label, attempt in ( ("body", lambda: mod.aes_gcm_decrypt(key, nonce, b"aad", tampered_body)), ("tag", lambda: mod.aes_gcm_decrypt(key, nonce, b"aad", tampered_tag)), ("aad", lambda: mod.aes_gcm_decrypt(key, nonce, b"other", packed)), ("key", lambda: mod.aes_gcm_decrypt(wrong_key, nonce, b"aad", packed)), ): with pytest.raises(RuntimeError, match="^AES-GCM authentication failed$"): attempt() with pytest.raises(RuntimeError, match="missing tag"): mod.aes_gcm_decrypt(key, nonce, b"aad", b"short") with pytest.raises(RuntimeError, match="nonce must be 12 bytes"): mod.aes_gcm_encrypt(key, bytes(16), b"", b"x") with pytest.raises(RuntimeError, match="key must be 32 bytes"): mod.aes_gcm_encrypt(bytes(16), nonce, b"", b"x") mod.select_crypto_backend() def test_hkdf_sha256_matches_rfc5869_vectors(mod): # RFC 5869 Appendix A, test cases 1 and 3. okm = mod.hkdf_sha256(b"\x0b" * 22, bytes(range(13)), bytes(range(0xF0, 0xFA)), 42) assert okm.hex() == ( "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" ) okm = mod.hkdf_sha256(b"\x0b" * 22, b"", b"", 42) assert okm.hex() == ( "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8" ) def test_diagnostics_report_the_backend_without_key_material(mod): status = mod.crypto_backend_status() assert status["crypto_backend"] in {"native", "pure_python"} assert set(status) <= {"crypto_backend", "native_crypto_available", "crypto_backend_note"}