This can cause TEEProverRegistry.registerSigner to revert with AttestationVerificationFailed even though the attestation satisfies the verifier's documented validity rule.
Vulnerability Details
The timestamp validation comment in NitroEnclaveVerifier._verifyJournal states:
But the actual check uses <= and >= as invalid conditions:
For the documented rule to hold, only strictly old or strictly future attestations should be rejected:
The current implementation rejects the exact equality cases that the documentation says are valid.
Impact
Valid Nitro enclave attestations can be incorrectly rejected as InvalidTimestamp. This can block otherwise valid signer registration through TEEProverRegistry.registerSigner.
The relevant integration path is:
Recommended Fix
Use strict invalidity checks so the documented valid boundaries are accepted:
This matches the documented invariant:
timestamp + maxTimeDiff >= block.timestamp
timestamp <= block.timestamp
Also consider reviewing TEEProverRegistry.registerSigner, which uses a similar inclusive cutoff for MAX_AGE:
If the intended policy is to reject exact-boundary timestamps, the comments should be updated to make the exclusive validity window explicit.
Proof of Concept
Add the PoC below to contracts/test/multiproof/NitroEnclaveVerifier.t.sol inside the existing NitroEnclaveVerifierTest contract.
They mock only the external RiscZero verifier call. The production NitroEnclaveVerifier.verify and _verifyJournal logic is executed.
// The timestamp validation converts milliseconds to seconds and checks:
// - Attestation is not too old (timestamp + maxTimeDiff >= block.timestamp)
// - Attestation is not from the future (timestamp <= block.timestamp)
if (journal.timestamp / MS_PER_SECOND + MAX_AGE <= block.timestamp) revert AttestationTooOld();
function testPoCValidAtExactBlockTimestampIsRejected() public {
_setUpRiscZeroConfig();
VerifierJournal memory journal = _createSuccessJournal();
// Documented valid boundary in NitroEnclaveVerifier:
// "Attestation is not from the future (timestamp <= block.timestamp)".
journal.timestamp = uint64(block.timestamp) * 1000;
bytes memory output = abi.encode(journal);
bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0));
_mockRiscZeroVerify(VERIFIER_ID, output, proofBytes);
vm.prank(submitter);
VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes);
assertEq(uint8(result.result), uint8(VerificationResult.InvalidTimestamp));
}
function testPoCValidAtMaxAgeBoundaryIsRejected() public {
_setUpRiscZeroConfig();
VerifierJournal memory journal = _createSuccessJournal();
// Documented valid boundary in NitroEnclaveVerifier:
// "Attestation is not too old (timestamp + maxTimeDiff >= block.timestamp)".
journal.timestamp = uint64(block.timestamp - MAX_TIME_DIFF) * 1000;
bytes memory output = abi.encode(journal);
bytes memory proofBytes = abi.encodePacked(bytes4(0), bytes32(0));
_mockRiscZeroVerify(VERIFIER_ID, output, proofBytes);
vm.prank(submitter);
VerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes);
assertEq(uint8(result.result), uint8(VerificationResult.InvalidTimestamp));
}
forge test --match-contract NitroEnclaveVerifierTest --match-test 'testPoCValidAtExactBlockTimestampIsRejected|testPoCValidAtMaxAgeBoundaryIsRejected' -vvv
Ran 2 tests for test/multiproof/NitroEnclaveVerifier.t.sol:NitroEnclaveVerifierTest
[PASS] testPoCValidAtExactBlockTimestampIsRejected()
[PASS] testPoCValidAtMaxAgeBoundaryIsRejected()
Suite result: ok. 2 passed; 0 failed; 0 skipped