Skip to content

Commit a29b5a4

Browse files
committed
fix(idemix): never return a nil error when credential verification fails
Signed-off-by: AkramBitar <akram@il.ibm.com>
1 parent 04845db commit a29b5a4

3 files changed

Lines changed: 160 additions & 1 deletion

File tree

docs/services/identity.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,14 @@ To accommodate different deployment structures, the Key Manager performs directo
268268
1. It first attempts to load the files directly from the configured directory (`<dir>`).
269269
2. If this fails, it appends an extra `msp` path element to the directory (i.e., `<dir>/msp/`) and tries again (e.g. searching for `<dir>/msp/msp/IssuerPublicKey` and `<dir>/msp/user/SignerConfig`).
270270

271+
##### Credential Verification at Load Time
272+
When the loaded signer configuration carries secret key material (user secret key plus credential),
273+
the Idemix Key Manager verifies the credential against the issuer public key while it is being
274+
constructed. A credential that does not verify — whether the underlying BCCSP reports the failure as
275+
an error or simply as a negative verification result — makes construction fail with
276+
`credential is not cryptographically valid`; no key manager is returned. Configurations without
277+
secret key material are loaded as verify-only (remote) key managers and skip this check.
278+
271279
#### 3. IdemixNym (Idemix with Pseudonym-based Identity)
272280
An extension of Idemix that uses a **commitment to the Enrollment ID (EID)** as the identity instead of the full Idemix signature.
273281
* **Identity (Payload)**: A small **Nym EID** (a cryptographic commitment to the enrollment ID, $g^{sk} \cdot h^{r_{eid}}$).

token/services/identity/idemix/km.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,16 @@ func NewKeyManagerWithSchema(
167167
},
168168
},
169169
)
170-
if err != nil || !valid {
170+
// Keep the two failure modes apart: the BCCSP may report an invalid credential either by
171+
// returning an error or by returning valid == false with a nil error. Wrapping a nil error
172+
// yields a nil error, which would turn a verification failure into a (nil, nil) return and
173+
// a nil-pointer panic in the caller.
174+
if err != nil {
171175
return nil, errors.WithMessagef(err, "credential is not cryptographically valid")
172176
}
177+
if !valid {
178+
return nil, errors.New("credential is not cryptographically valid")
179+
}
173180
logger.Debugf("the signer contains key material, load it, done.")
174181
} else {
175182
logger.Debugf("the signer does not contain full key material, it will be considered remote [cred=%d,sk=%d]", len(conf.Signer.Cred), len(conf.Signer.Sk))

token/services/identity/idemix/km_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/LFDT-Panurus/panurus/token/services/logging"
2727
kvs2 "github.com/LFDT-Panurus/panurus/token/services/storage/db/kvs"
2828
"github.com/LFDT-Panurus/panurus/token/services/utils"
29+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
2930
_ "github.com/hyperledger-labs/fabric-smart-client/platform/view/services/storage/driver/memory"
3031
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/view"
3132
"github.com/stretchr/testify/assert"
@@ -971,3 +972,146 @@ func testKeyManager_DeserializeSigningIdentityNoProbeRemote(t *testing.T, config
971972
_, err = remoteKM.DeserializeSigningIdentityNoProbe(t.Context(), identityDescriptor.Identity)
972973
require.Error(t, err)
973974
}
975+
976+
// verifyOverridingCSP decorates a real bccsp.BCCSP and forces the outcome of credential
977+
// verification, so tests can exercise both ways the BCCSP reports a bad credential: a plain
978+
// `false` with no error, and a `false` accompanied by an error.
979+
type verifyOverridingCSP struct {
980+
types.BCCSP
981+
valid bool
982+
verifyErr error
983+
}
984+
985+
func (c *verifyOverridingCSP) Verify(k types.Key, signature, digest []byte, opts types.SignerOpts) (bool, error) {
986+
if _, ok := opts.(*types.IdemixCredentialSignerOpts); ok {
987+
return c.valid, c.verifyErr
988+
}
989+
990+
return c.BCCSP.Verify(k, signature, digest, opts)
991+
}
992+
993+
// TestNewKeyManagerCredentialVerificationFailure is a regression test for the case where the BCCSP
994+
// reports a cryptographically invalid credential through its boolean return value alone
995+
// (valid == false, err == nil). The constructor used to funnel both failure modes through
996+
// errors.WithMessagef(err, ...), which returns nil when the wrapped error is nil, so it returned
997+
// (nil, nil): callers checking only err != nil went on to use a nil *KeyManager and panicked
998+
// later, away from the real cause.
999+
func TestNewKeyManagerCredentialVerificationFailure(t *testing.T) {
1000+
for _, curve := range []struct {
1001+
configPath string
1002+
curveID math.CurveID
1003+
}{
1004+
{"./testdata/bls12_381_bbs_gurvy/idemix", math.BLS12_381_BBS_GURVY},
1005+
{"./testdata/bls12_381_bbs/idemix", math.BLS12_381_BBS_GURVY},
1006+
} {
1007+
for _, tc := range []struct {
1008+
name string
1009+
valid bool
1010+
verifyErr error
1011+
// contains lists substrings the returned error must mention
1012+
contains []string
1013+
}{
1014+
{
1015+
// the bug: failure signalled by the boolean only
1016+
name: "invalid credential, no error from the BCCSP",
1017+
valid: false,
1018+
contains: []string{"credential is not cryptographically valid"},
1019+
},
1020+
{
1021+
name: "invalid credential, error from the BCCSP",
1022+
valid: false,
1023+
verifyErr: errors.New("verification exploded"),
1024+
contains: []string{"credential is not cryptographically valid", "verification exploded"},
1025+
},
1026+
{
1027+
// an error alongside valid == true is still a failure
1028+
name: "error from the BCCSP with valid set",
1029+
valid: true,
1030+
verifyErr: errors.New("verification exploded"),
1031+
contains: []string{"credential is not cryptographically valid", "verification exploded"},
1032+
},
1033+
} {
1034+
t.Run(tc.name+" ["+curve.configPath+"]", func(t *testing.T) {
1035+
kvs, err := kvs2.NewInMemory()
1036+
require.NoError(t, err)
1037+
config, err := crypto.NewConfig(curve.configPath)
1038+
require.NoError(t, err)
1039+
keyStore, err := crypto.NewKeyStore(curve.curveID, kvs2.Keystore(kvs))
1040+
require.NoError(t, err)
1041+
realCSP, err := crypto.NewBCCSP(keyStore, curve.curveID)
1042+
require.NoError(t, err)
1043+
1044+
keyManager, err := NewKeyManager(
1045+
config,
1046+
types.EidNymRhNym,
1047+
&verifyOverridingCSP{BCCSP: realCSP, valid: tc.valid, verifyErr: tc.verifyErr},
1048+
)
1049+
// the invariant callers rely on: a failure is never reported as (nil, nil)
1050+
require.Error(t, err, "credential verification failure must return a non-nil error")
1051+
require.Nil(t, keyManager)
1052+
for _, substring := range tc.contains {
1053+
require.ErrorContains(t, err, substring)
1054+
}
1055+
})
1056+
}
1057+
}
1058+
}
1059+
1060+
// TestNewKeyManagerCredentialVerificationSuccess pins the happy path through the same decorator,
1061+
// proving the failure cases above are caused by the verification outcome and not by the decorator
1062+
// itself.
1063+
func TestNewKeyManagerCredentialVerificationSuccess(t *testing.T) {
1064+
const configPath = "./testdata/bls12_381_bbs_gurvy/idemix"
1065+
kvs, err := kvs2.NewInMemory()
1066+
require.NoError(t, err)
1067+
config, err := crypto.NewConfig(configPath)
1068+
require.NoError(t, err)
1069+
keyStore, err := crypto.NewKeyStore(math.BLS12_381_BBS_GURVY, kvs2.Keystore(kvs))
1070+
require.NoError(t, err)
1071+
realCSP, err := crypto.NewBCCSP(keyStore, math.BLS12_381_BBS_GURVY)
1072+
require.NoError(t, err)
1073+
1074+
keyManager, err := NewKeyManager(
1075+
config,
1076+
types.EidNymRhNym,
1077+
&verifyOverridingCSP{BCCSP: realCSP, valid: true},
1078+
)
1079+
require.NoError(t, err)
1080+
require.NotNil(t, keyManager)
1081+
}
1082+
1083+
// TestNewKeyManagerTamperedCredential drives the same failure end to end, with no test double: a
1084+
// byte of the real credential is flipped so the real BCCSP rejects it. Whichever way the BCCSP
1085+
// signals the rejection, construction must fail loudly rather than hand back a nil key manager.
1086+
func TestNewKeyManagerTamperedCredential(t *testing.T) {
1087+
testNewKeyManagerTamperedCredential(t, "./testdata/bls12_381_bbs_gurvy/idemix", math.BLS12_381_BBS_GURVY)
1088+
testNewKeyManagerTamperedCredential(t, "./testdata/bls12_381_bbs/idemix", math.BLS12_381_BBS_GURVY)
1089+
}
1090+
1091+
func testNewKeyManagerTamperedCredential(t *testing.T, configPath string, curveID math.CurveID) {
1092+
t.Helper()
1093+
kvs, err := kvs2.NewInMemory()
1094+
require.NoError(t, err)
1095+
config, err := crypto.NewConfig(configPath)
1096+
require.NoError(t, err)
1097+
keyStore, err := crypto.NewKeyStore(curveID, kvs2.Keystore(kvs))
1098+
require.NoError(t, err)
1099+
cryptoProvider, err := crypto.NewBCCSP(keyStore, curveID)
1100+
require.NoError(t, err)
1101+
1102+
// sanity check: the untouched credential is accepted
1103+
keyManager, err := NewKeyManager(config, types.EidNymRhNym, cryptoProvider)
1104+
require.NoError(t, err)
1105+
require.NotNil(t, keyManager)
1106+
1107+
// tamper with the credential's signature material
1108+
config, err = crypto.NewConfig(configPath)
1109+
require.NoError(t, err)
1110+
require.NotEmpty(t, config.Signer.Cred)
1111+
config.Signer.Cred[len(config.Signer.Cred)-1] ^= 0xFF
1112+
1113+
keyManager, err = NewKeyManager(config, types.EidNymRhNym, cryptoProvider)
1114+
require.Error(t, err, "a tampered credential must return a non-nil error")
1115+
require.Nil(t, keyManager)
1116+
require.ErrorContains(t, err, "credential is not cryptographically valid")
1117+
}

0 commit comments

Comments
 (0)