mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
identityvm: judge credential expiry at block time, and stop letting a missing
proof satisfy RequireZKProofs Two defects. 1. Block.verifyCredential compared cred.ExpirationDate against time.Now() inside block verification. The verdict therefore depended on when a node happened to verify: the same block is valid before the expiry instant and invalid after, so two validators either side of it disagree. Worse, it is unconditional on replay — a node bootstrapping the chain rejects every historical block whose credentials have since expired, which after enough time is all of them. Proven: a block timestamped 48h ago carrying a credential that was live then failed verification. Now compared against b.BlockTimestamp, the one clock every node agrees on, already bounded against the parent and local time earlier in Verify. 2. VerifyCredential gated the ZK requirement on the presence of the thing it requires: `if vm.config.RequireZKProofs && cred.Proof != nil`. A credential carrying no proof object skipped the branch and returned (true, nil) on a chain configured to require proofs. Proven: VerifyCredential returned true for a proofless credential with RequireZKProofs set. The requirement now holds whenever it is configured. Tests: TestCredentialExpiryIsJudgedAtBlockTime, TestRequireZKProofsCannotBeSkippedByOmittingTheProof. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+7
-2
@@ -121,8 +121,13 @@ func (b *Block) verifyCredential(cred *Credential) error {
|
||||
return errors.New("too many claims")
|
||||
}
|
||||
|
||||
// Verify expiration is in future
|
||||
if time.Now().After(cred.ExpirationDate) {
|
||||
// The credential must be unexpired AS OF THIS BLOCK. Comparing against the
|
||||
// wall clock would make the verdict depend on when a node happens to verify:
|
||||
// the same block is valid before the expiry and invalid after it, and a node
|
||||
// replaying history during bootstrap rejects every block whose credentials
|
||||
// have since expired. The block timestamp is the only clock every node agrees
|
||||
// on, and Verify has already bounded it against the parent and local time.
|
||||
if time.Unix(b.BlockTimestamp, 0).After(cred.ExpirationDate) {
|
||||
return errors.New("credential already expired")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package identityvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// credentialExpiringAt builds a self-issued credential with the given expiry.
|
||||
func credentialExpiringAt(expiry time.Time) *Credential {
|
||||
return &Credential{
|
||||
ID: ids.GenerateTestID(),
|
||||
Type: []string{"VerifiableCredential"},
|
||||
Issuer: ids.GenerateTestID(),
|
||||
Subject: ids.GenerateTestID(),
|
||||
IssuanceDate: expiry.Add(-time.Hour),
|
||||
ExpirationDate: expiry,
|
||||
Claims: map[string]interface{}{"name": "test"},
|
||||
Status: CredentialActive,
|
||||
}
|
||||
}
|
||||
|
||||
// TestCredentialExpiryIsJudgedAtBlockTime pins credential expiry to the block
|
||||
// timestamp rather than the wall clock. Against time.Now() the same block is
|
||||
// valid before the expiry and invalid after it, so two nodes verifying either
|
||||
// side of that instant disagree — and a node replaying history during bootstrap
|
||||
// rejects every historical block whose credentials have since expired, which is
|
||||
// every one of them.
|
||||
func TestCredentialExpiryIsJudgedAtBlockTime(t *testing.T) {
|
||||
require := require.New(t)
|
||||
vm := setupTestVM(t)
|
||||
defer vm.Shutdown(context.Background())
|
||||
|
||||
// A block from the past carrying a credential that was valid then and has
|
||||
// since expired. Replaying it must still verify.
|
||||
blockTime := time.Now().Add(-48 * time.Hour)
|
||||
historical := &Block{
|
||||
BlockHeight: 1,
|
||||
BlockTimestamp: blockTime.Unix(),
|
||||
Credentials: []*Credential{credentialExpiringAt(blockTime.Add(time.Hour))},
|
||||
vm: vm,
|
||||
}
|
||||
require.NoError(historical.verifyCredential(historical.Credentials[0]),
|
||||
"a credential live at the block's timestamp must verify when replayed later")
|
||||
|
||||
// A block that carries a credential already expired at its own timestamp is
|
||||
// still refused.
|
||||
stale := &Block{
|
||||
BlockHeight: 1,
|
||||
BlockTimestamp: blockTime.Unix(),
|
||||
Credentials: []*Credential{credentialExpiringAt(blockTime.Add(-time.Hour))},
|
||||
vm: vm,
|
||||
}
|
||||
require.Error(stale.verifyCredential(stale.Credentials[0]))
|
||||
}
|
||||
|
||||
// TestRequireZKProofsCannotBeSkippedByOmittingTheProof proves the ZK requirement
|
||||
// is not gated on the presence of the thing it requires. A credential with no
|
||||
// proof object verified under RequireZKProofs, so the setting was bypassed by
|
||||
// simply not supplying a proof.
|
||||
func TestRequireZKProofsCannotBeSkippedByOmittingTheProof(t *testing.T) {
|
||||
require := require.New(t)
|
||||
vm := setupTestVM(t)
|
||||
defer vm.Shutdown(context.Background())
|
||||
|
||||
vm.config.RequireZKProofs = true
|
||||
|
||||
noProof := credentialExpiringAt(time.Now().Add(time.Hour))
|
||||
emptyProof := credentialExpiringAt(time.Now().Add(time.Hour))
|
||||
emptyProof.Proof = &CredentialProof{Type: "zk"}
|
||||
withProof := credentialExpiringAt(time.Now().Add(time.Hour))
|
||||
withProof.Proof = &CredentialProof{Type: "zk", ZKProof: []byte("proof-bytes")}
|
||||
|
||||
vm.mu.Lock()
|
||||
for _, c := range []*Credential{noProof, emptyProof, withProof} {
|
||||
vm.credentials[c.ID] = c
|
||||
}
|
||||
vm.mu.Unlock()
|
||||
|
||||
ok, err := vm.VerifyCredential(noProof.ID)
|
||||
require.False(ok, "a credential with no proof must not satisfy RequireZKProofs")
|
||||
require.ErrorIs(err, errInvalidProof)
|
||||
|
||||
ok, err = vm.VerifyCredential(emptyProof.ID)
|
||||
require.False(ok)
|
||||
require.ErrorIs(err, errInvalidProof)
|
||||
|
||||
ok, err = vm.VerifyCredential(withProof.ID)
|
||||
require.NoError(err)
|
||||
require.True(ok)
|
||||
}
|
||||
+5
-4
@@ -636,12 +636,13 @@ func (vm *VM) VerifyCredential(credID ids.ID) (bool, error) {
|
||||
return false, errCredentialRevoked
|
||||
}
|
||||
|
||||
// Verify ZK proof if required
|
||||
if vm.config.RequireZKProofs && cred.Proof != nil {
|
||||
if len(cred.Proof.ZKProof) == 0 {
|
||||
// When the chain requires ZK proofs, a credential must carry one. Gating the
|
||||
// requirement on cred.Proof != nil made the requirement optional: a credential
|
||||
// with no proof object at all skipped the check and verified.
|
||||
if vm.config.RequireZKProofs {
|
||||
if cred.Proof == nil || len(cred.Proof.ZKProof) == 0 {
|
||||
return false, errInvalidProof
|
||||
}
|
||||
// Would verify ZK proof here
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
Reference in New Issue
Block a user