|
| 1 | +# SPDX-License-Identifier: MPL-2.0 |
| 2 | +# Soundness regression tests — guard against reintroducing the P1 soundness holes: |
| 3 | +# G02: vacuous NoNaN static "proof" (has_safe_operations == true) |
| 4 | +# G01: certificate falsely labelling an unkeyed digest as "SHA256-HMAC" |
| 5 | +# These run inside the main suite so the holes cannot silently reopen. |
| 6 | + |
| 7 | +using Test |
| 8 | +using Axiom |
| 9 | +using JSON |
| 10 | + |
| 11 | +@testset "Soundness (P1 holes)" begin |
| 12 | + model = Sequential(Dense(10, 5, relu), Dense(5, 3), Softmax()) |
| 13 | + |
| 14 | + @testset "NoNaN is not a vacuous static proof (G02)" begin |
| 15 | + # Structure alone cannot soundly prove NoNaN (a finite-input guarantee is |
| 16 | + # also required), so the static path must return :unknown, never :proven. |
| 17 | + @test Axiom.try_static_verify(NoNaN(), model) === :unknown |
| 18 | + # The unconditional `true` is gone; the honest predicate makes no claim. |
| 19 | + @test Axiom.has_safe_operations(model) === false |
| 20 | + # Regression: genuinely-sound static proofs still work. |
| 21 | + @test Axiom.try_static_verify(ValidProbabilities(), model) === :proven |
| 22 | + # NoNaN still verifies empirically on well-behaved data (fallback path). |
| 23 | + x = Tensor(randn(Float32, 4, 10)) |
| 24 | + res = verify(model; properties = [NoNaN()], data = [(x, nothing)]) |
| 25 | + @test res.passed |
| 26 | + end |
| 27 | + |
| 28 | + @testset "Certificate does not falsely claim a keyed signature (G01)" begin |
| 29 | + x = Tensor(randn(Float32, 2, 10)) |
| 30 | + result = verify(model; properties = [ValidProbabilities(), FiniteOutput()], data = [(x, nothing)]) |
| 31 | + cert = generate_certificate(model, result; model_name = "soundness-ci") |
| 32 | + |
| 33 | + path = tempname() * ".json" |
| 34 | + save_certificate(cert, path; format = :json) |
| 35 | + try |
| 36 | + content = read(path, String) |
| 37 | + # The false "SHA256-HMAC" label (unkeyed digest sold as HMAC) is gone. |
| 38 | + @test !occursin("SHA256-HMAC", content) |
| 39 | + parsed = JSON.parsefile(path) |
| 40 | + @test parsed["signature"]["authenticated"] == false |
| 41 | + @test parsed["signature"]["kind"] == "content-digest" |
| 42 | + finally |
| 43 | + rm(path; force = true) |
| 44 | + end |
| 45 | + |
| 46 | + # Tamper-evidence: an altered digest is rejected. (A forger who recomputes |
| 47 | + # the digest still passes — the documented limit of a content digest; |
| 48 | + # authenticating Ed448+Dilithium5 signatures are tracked in ROADMAP.adoc.) |
| 49 | + @test verify_certificate(cert) |
| 50 | + forged = Axiom.Certificate(cert.model_hash, cert.model_name, cert.properties, |
| 51 | + cert.verification_mode, cert.test_data_hash, cert.proof_type, |
| 52 | + cert.created_at, cert.axiom_version, cert.verifier_id, "deadbeef") |
| 53 | + @test !verify_certificate(forged) |
| 54 | + end |
| 55 | +end |
0 commit comments