This directory contains an anonymous credentials implementation using the composite proof system.
All the objects described below have semantic versioning and can be accessing using this.version
.
Specifies all the attributes of the credential and their structure (nesting). Because anonymous credentials allow to hide any number of attributes of the credential, it must be possible to know what attributes were part of the issued credential. Schema also defines the encoding of each attribute. Encoding determines how the credential attribute value must be converted to a positive integer (prime field, a finite field of prime order, to be precise) before passing to the crypto (signing, proving) algorithms. Choosing an appropriate encoding process is essential when doing predicates like enforcing bounds on attributes (range proofs), verifiable encryption of attributes or when using it in predicates written in Circom. For more details on the need of encoding see here and here. It expects the schema in the JSON-schema syntax, draft-07. The schema can define the attributes as literals (string, numbers, datetime) or as objects or arrays.
A CredentialSchema
object can either have embedded schema or non-embedded scheme. In the former case, the attributes and their encodings
are expected to be present in the properties
key of the schema whereas in the latter, these are supposed to be fetched from a url specified
in the $id
key. The latter is useful when a uniform scheme needs to be enforced in all credentials.
Example of creating an embedded schema. For each attribute, its encoding is determined by the type
and other parameters.
Schema properties
must have the key credentialSubject
// Get essential properties
const jsonSchema = CredentialSchema.essential();
// Set custom properties under the top level key `credentialSubject`
jsonSchema.properties.credentialSubject = {
type: 'object',
properties: {
fname: { type: 'string' },
score: { type: 'integer', minimum: -100 }, // attribute `score` can have a minimum value of -100 and is always an integer
long: { type: 'number', minimum: 0, multipleOf: 0.01 }, // attribute `long` can have a minimum value of 0.00 and has 2 decimal places so it can have values like 0.45, 4.25, 90.68, etc
l1: { type: 'decimalNumber', minimum: -180, decimalPlaces: 3 }, // attribute `l1` can have a minimum value of -180.000 and has 3 decimal places so it can have values like -100.456, 104.251, 90.680, etc
l2: { type: 'positiveDecimalNumber', decimalPlaces: 1 }, // attribute `l2` can have a minimum value of 0.0 and has 1 decimal places so it can have values like 100.4, 5.2, 9.6, etc
dateOfRegistration: { type: 'string', format: 'date' }, // attribute `dateOfRegistration` needs to specify a valid date like 1999-01-01
timeOfBirth: { type: 'string', format: 'date-time' }, // attribute `timeOfBirth` needs to specify a valid date-time like 2023-09-14T19:26:40.488Z
SSN: { $ref: '#/definitions/encryptableString' }, // attribute `SSN` can be (verifiably) encrypted so instructs the `CredentialSchema` to encode in a appropriate manner. See main Readme for such encoding
}
};
const schema = new CredentialSchema(jsonSchema);
console.assert(schema.hasEmbeddedJsonSchema());
Example of creating a non-embedded schema. The schema will be fetched from the url https://example.com?hash=abc123ff
using
the callback schemaGetter
(defined by the caller) which expects the url as its only parameter. The url in the $id
field doesn't have to be an HTTP(S) url but could be any identifier which the callback schemaGetter
is capable of using.
const nonEmbeddedSchema = {
$id: 'https://example.com?hash=abc123ff',
[META_SCHEMA_STR]: 'http://json-schema.org/draft-07/schema#',
type: 'object',
};
const schema = await CredentialSchema.newSchemaFromExternal(nonEmbeddedSchema, schemaGetter);
console.assert(schema.hasEmbeddedJsonSchema());
Schema can be converted to and from JSON
// Convert to JSON
const j = schema.toJSON();
// Recreate from JSON
const s = CredentialSchema.fromJSON(j);
A schema could be generated by looking at the credential attributes. Say a JSON object cred
is created with the necessary attributes, then
the following will create the corresponding schema object.
// Set whatever properties are known for sure
const jsonSchema = CredentialSchema.essential();
// Rest of the properties are generated using `cred`
const schema = CredentialSchema.generateAppropriateSchema(
cred,
new CredentialSchema(jsonSchema)
);
While creating schema, parsing options can be passed when can allow scheme generation to use defaults and/or set the defaults for numeric values
const schema = new CredentialSchema(jsonSchema, { useDefaults: true, defaultDecimalPlaces: 5 });
Schema keys can also be nested objects or arrays. The following has an array of nested objects and some top level keys in addition to credentialSubject
const jsonSchema = CredentialSchema.essential();
const item = {
type: 'object',
properties: {
name: { type: 'string' },
location: {
type: 'object',
properties: {
name: { type: 'string' },
geo: {
type: 'object',
properties: {
lat: { type: 'number', minimum: -90, multipleOf: 0.001 },
long: { type: 'number', minimum: -180, multipleOf: 0.001 }
}
}
}
}
}
};
// Set the credentialSubject to an array of nested objects of size 3. The array size must be known
jsonSchema.properties[credentialSubject] = {
type: 'array',
items: [item, item, item]
};
// Set a top level key `issuer`
jsonSchema.properties['issuer'] = {
type: 'object',
properties: {
name: { type: 'string' },
desc: { type: 'string' },
logo: { type: 'string' }
}
};
// Set more top level keys
jsonSchema.properties['issuanceDate'] = { type: 'string', format: 'date' };
jsonSchema.properties['expirationDate'] = { type: 'string', format: 'date' };
const schema = new CredentialSchema(jsonSchema);
A credential contains one or more attributes signed by the issuer; attributes and the signature together make up the credential. Anonymous credentials allow to hide any number of attributes and the signature (always) while proving the knowledge of the signature by the issuer. A credential always contains a schema as one of the attribute (inline, not a reference) and the schema attribute is always revealed to the verifier. Some other attributes (which can be considered metadata) are always revealed like signature type and credential status type.
A CredentialBuilder is used to build a credential by setting various attributes and
then signed using the issuer's secret key resulting in a Credential which can then be verified using the
public key of the issuer. A credential might have a credentialStatus
field indicating whether the credential can be revoked or not. Currently only 1
mechanism is supported and that is accumulator but the credentialStatus
property is oblivious to that. However, there are several kinds of
accumulators that we support. A VB positive accumulator supports only membership proofs but a VB universal accumulator supports non-membership proofs
as well. A KB universal accumulator supports both membership and non-membership proofs but the non-membership proofs are relatively efficient than VB
accumulator. However, creating and updating the accumulator has double the cost which is acceptable since done by the signer/revocation authority. This is
because a KB universal accumulator is composed of 2 VB positive accumulators.
The following creates a schema, and then a credential from the schema. The example uses BBSCredentialBuilder
, a subclass of CredentialBuilder
, for BBS signatures
but other schemes work similarly. Calling BBSCredentialBuilder.sign
will return a BBSCredential
object.
// Signer's one time setup, creates params and keys.
// The number 1 doesn't matter for BBS, BBS+, BBDT16 as params can be extended
const params = BBSSignatureParams.generate(1, SignatureLabelBytes);
// Create secret and public keys
const keypair = BBSKeypair.generate(params);
const sk = keypair.sk;
const pk = keypair.pk;
// Create schema for credential
const schema = CredentialSchema.essential();
schema.properties[credentialSubject] = {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
sensitive: {
type: 'object',
properties: {
email: { type: 'string' },
phone: { type: 'string' },
SSN: { $ref: '#/definitions/encryptableString' }
}
}
}
};
const credSchema = new CredentialSchema(schema);
// Set schema
const builder = new BBSCredentialBuilder();
builder.schema = credSchema;
// Set attributes and sign
builder.subject = {
fname: 'John',
lname: 'Smith',
sensitive: {
phone: '810-1234567',
email: '[email protected]',
SSN: '123-456789-0'
}
};
const cred = builder.sign(sk);
// Verify credential using public key
console.assert(cred.verify(pk).verified);
Credential can be converted to and from JSON
const j = cred.toJSON();
const c = BBSCredential.fromJSON(credJson);
Schema for credentials can be generated by inferring from the credential object by setting the parameter requireSameFieldsAsSchema
to false. The example
below generates a bare minimum schema and does not specify any credential attributes but its possible to specify only some attribute types/encodings in
the schema and let the rest be autogenerated.
const builder = new BBSCredentialBuilder();
// Set the credential attributes
builder.subject = {
fname: 'John',
lname: 'Smith',
city: 'NY',
education: { university: 'Example', major: 'Nothing' },
someNumber: 2.5,
someInteger: 5,
};
// Generate a bare minimum schema
builder.schema = new CredentialSchema(CredentialSchema.essential(), { useDefaults: true });
// Before signing, generate schema for the credential attributes
const cred = builder.sign(sk, undefined, { requireSameFieldsAsSchema: false });
// Verify credential using public key
console.assert(cred.verify(pk).verified);
Its possible to specify top level fields, i.e. attributes at the same level as credentialSubject
and not under it. Also attributes
can be arrays as shown below.
const jsonSchema = CredentialSchema.essential();
const item = {
type: 'object',
properties: {
name: { type: 'string' },
location: {
type: 'object',
properties: {
name: { type: 'string' },
geo: {
type: 'object',
properties: {
lat: { type: 'number', minimum: -90, multipleOf: 0.001 },
long: { type: 'number', minimum: -180, multipleOf: 0.001 }
}
}
}
}
}
};
jsonSchema.properties[credentialSubject] = {
type: 'array',
items: [item, item, item]
};
jsonSchema.properties['issuer'] = {
type: 'object',
properties: {
name: { type: 'string' },
desc: { type: 'string' },
logo: { type: 'string' }
}
};
jsonSchema.properties['issuanceDate'] = { type: 'string', format: 'date' };
jsonSchema.properties['expirationDate'] = { type: 'string', format: 'date' };
const credSchema = new CredentialSchema(jsonSchema);
const builder = new CredentialBuilder();
builder.schema = credSchema;
builder.subject = [
{
name: 'Random',
location: {
name: 'Somewhere',
geo: {
lat: -23.658,
long: 2.556
}
}
},
{
name: 'Random-1',
location: {
name: 'Somewhere-1',
geo: {
lat: 35.01,
long: -40.987
}
}
},
{
name: 'Random-2',
location: {
name: 'Somewhere-2',
geo: {
lat: -67.0,
long: -10.12
}
}
}
];
builder.setTopLevelField('issuer', {
name: 'An issuer',
desc: 'Just an issuer',
logo: 'https://images.example-issuer.com/logo.png'
});
builder.setTopLevelField('issuanceDate', 1662010849700);
builder.setTopLevelField('expirationDate', 1662011950934);
const cred = builder.sign(sk);
For credentials that can be revoked (see main Readme for background on revocation), its credentialStatus
field must be set accordingly
by calling setCredentialStatus
. In the following example, dock:accumulator:accumId123
is the unique id of the accumulator,
MEM_CHECK_STR
means that revocation status should check membership in the accumulator, user:A-123
is the unique id of the
credential that is put in the accumulator and RevocationStatusProtocol.Vb22
means that VB22 accumulator is used.
const builder = new BBSCredentialBuilder();
builder.schema = credSchema;
builder.subject = {
// .... attributes
};
builder.setCredentialStatus('dock:accumulator:accumId123', MEM_CHECK_STR, 'user:A-123', RevocationStatusProtocol.Vb22);
const cred = builder.sign(sk);
See these tests for examples of credential issuance, verification and (de)serialization.
A user/holder might have any number of credentials. To convince a verifier that he has the credentials by certain issuers and
optionally reveal some attributes from across the credentials or prove certain properties about the attributes, it creates a
presentation. Similar to credentials, these follow builder pattern and thus a PresentationBuilder
is used to create a Presentation. The builder lets you add credentials (using addCredential
), reveal attributes (using markAttributesRevealed
),
prove various attributes equal without revealing them (using enforceAttributeEquality
), prove attributes inequal to a public value (using enforceAttributeInequality
),
enforce bounds on attributes (using enforceBounds
), verifiably encrypt attributes (using verifiablyEncrypt
), enforce predicates written
in Circom (using enforceCircomPredicate
).
The PresentationBuilder
allows adding a context
for specifying things like purpose of the presentation or any self attested claims
or anything else and a nonce
for replay protection.
As part a Presentation
, included is a PresentationSpecification which
specifies what the presentation is proving like what credentials, what's being revealed, which attributes are being proven equal,
bounds being enforced, attributes being encrypted and their ciphertext, accumulator used, etc. PresentationSpecification
describes
what is being cryptographically proved, not what the verifier is expecting the holder to prove. Thus, the verifier must check if the presentation
is indeed proving what it needs to be proven. Eg, verifier needs the holder to satisfy the bounds [30, 45) on a attribute but the holder
decides to satisfy [10, 15). Now the PresentationSpecification
will specify the bounds as 10 and 15 and the verifier should detect that they
are not what it asked for and reject the presentation.
Note that any binary values needed in the Presentation
JSON are encoded as base58.
The following example creates a presentation from 2 credentials, reveals attributes and proves certain attributes among them equal.
The holder has 2 credentials, credential1
and credential2
as shown. Both are BBSCredential
s but a presentation can accept
credentials of any type, eg. a presentation can have 4 credentials, 2 BBSCredential
s, 1 BBSPlusCredential
s and 1 PSCredential
.
Also, these can be from different signers (have different public keys).
A presentation assigns a unique 0-based index to each credential with a credential added. This index is then used to refer to
that credential when revealing attributes (with markAttributesRevealed
) or enforcing predicates (with enforceAttributeEquality
, enforceAttributeInequality
, etc)
Predicates satisfied in the presentation often contain a field protocol
which refers to the crypto protocol used to prove the predicate.
Eg. for proving inequality, the following shows Uprove
which is the identifier of the protocol. This is to allow
multiple protocols for proving the predicate and each application can choose the protocol which is appropriate for its needs.
Examples from now on will use a helper areBothEqual
to compare "any" kind of objects/arrays for equality.
const jsonSchema1 = CredentialSchema.essential();
jsonSchema1.jsonSchema[SUBJECT_STR] = {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
email: { type: 'string' },
SSN: { $ref: '#/definitions/encryptableString' },
userId: { $ref: '#/definitions/encryptableCompString' },
country: { type: 'string' },
city: { type: 'string' },
timeOfBirth: { type: 'integer', minimum: 0 },
score: { type: 'number', minimum: -100, multipleOf: 0.1 },
secret: { type: 'string' }
}
};
// Signer 1 with keys `sk1`, `pk1`, creates `credential1`
const builder1 = new BBSCredentialBuilder();
builder1.schema = new CredentialSchema(jsonSchema1);
builder1.subject = {
fname: 'John',
lname: 'Smith',
email: '[email protected]',
SSN: '123-456789-0',
userId: 'user:123-xyz-#',
country: 'USA',
city: 'New York',
timeOfBirth: 1662010849619,
score: -13.5,
secret: 'my-secret-that-wont-tell-anyone'
};
const credential1 = builder1.sign(sk1);
const jsonSchema2 = CredentialSchema.essential();
jsonSchema2.properties[SUBJECT_STR] = {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
isbool: { type: 'boolean' },
sensitive: {
type: 'object',
properties: {
secret: { type: 'string' },
email: { type: 'string' },
SSN: { $ref: '#/definitions/encryptableString' },
userId: { $ref: '#/definitions/encryptableCompString' }
}
},
location: {
type: 'object',
properties: {
country: { type: 'string' },
city: { type: 'string' }
}
},
timeOfBirth: { type: 'integer', minimum: 0 },
physical: {
type: 'object',
properties: {
height: { type: 'number', minimum: 0, multipleOf: 0.1 },
weight: { type: 'number', minimum: 0, multipleOf: 0.1 },
BMI: { type: 'number', minimum: 0, multipleOf: 0.01 }
}
},
score: { type: 'number', multipleOf: 0.1, minimum: -100 }
}
};
// Signer 2 with keys `sk2`, `pk2`, creates `credential2`
const builder2 = new BBSCredentialBuilder();
builder2.schema = new CredentialSchema(jsonSchema2);;
builder2.subject = {
fname: 'John',
lname: 'Smith',
isbool: true,
sensitive: {
secret: 'my-secret-that-wont-tell-anyone',
email: '[email protected]',
SSN: '123-456789-0',
userId: 'user:123-xyz-#'
},
location: {
country: 'USA',
city: 'New York'
},
timeOfBirth: 1662010849619,
physical: {
height: 181.5,
weight: 210,
BMI: 23.25
},
score: -13.5
};
const credential2 = builder2.sign(sk2);
// Holder starts creating presentation
const presBuilder = new PresentationBuilder();
// PresentationBuilder assigns index 0 to credential1
console.assert(presBuilder.addCredential(credential1) === 0);
// PresentationBuilder assigns index 1 to credential2
console.assert(presBuilder.addCredential(credential2) === 1);
// Reveal 2 attributes of credential1. Note that the attribute name needs to be provided in a flattened manner, i.e. <top level key>.<next level key>...<innermost key>
presBuilder.markAttributesRevealed(
0, // credential1 has index 0 as it was added first
new Set<string>(['credentialSubject.fname', 'credentialSubject.lname'])
);
// Reveal 3 attributes of credential2.
presBuilder.markAttributesRevealed(
1, // credential2 has index 1 as it was added second
new Set<string>([
'credentialSubject.fname',
'credentialSubject.location.country',
'credentialSubject.physical.BMI'
])
);
// Enforce equality of attribute credentialSubject.SSN of credential1 and credentialSubject.sensitive.SSN of credential2
presBuilder.enforceAttributeEquality(
[0, 'credentialSubject.SSN'], // credential1 has index 0
[1, 'credentialSubject.sensitive.SSN'] // credential2 has index 1
);
// Enforce equality of attribute credentialSubject.city of credential1 and credentialSubject.location.city of credential2
presBuilder.enforceAttributeEquality(
[0, 'credentialSubject.city'], // credential1 has index 0
[1, 'credentialSubject.location.city'] // credential2 has index 1
);
// Enforce that attribute credentialSubject.email of credential1 is not equal to [email protected]
presBuilder.enforceAttributeInequality(
0, // credential1 has index 0
'credentialSubject.email',
'[email protected]'
);
// Enforce that attribute credentialSubject.email of credential1 is not equal to [email protected]
presBuilder.enforceAttributeInequality(
0, // credential1 has index 0
'credentialSubject.email',
'[email protected]'
);
// Can contain metadata about the presentation like terms of use or any holder specified data called self-attested attributes. Could be a JSON string as well
presBuilder.context = 'some context';
// A nonce given by the verifier
presBuilder.nonce = new Uint8Array([0, 1, 100, 250, ...]);
// Generate the presentation
const pres = presBuilder.finalize();
// In addition to checking the cryptographic validity of the presentation `pres`, the verifier checks if the revealed attributes
// and predicates match his expectation
// Verifier checks the context and nonce. The exact checking of context will depend on the application
console.assert(pres.context === 'some context');
console.assert(areBothEqual(pres.nonce, new Uint8Array([0, 1, 100, 250, ...])));
// Presentation contains 2 credentials
console.assert(pres.spec.credentials.length === 2);
// Presentation's first credential reveals the expected attributes.
console.assert(
areBothEqual(
pres.spec.credentials[0].revealedAttributes, // 0 refers to first credential, which is credential1
{
credentialSubject: {
fname: 'John',
lname: 'Smith'
}
}
)
);
// Presentation's second credential reveals the expected attributes.
console.assert(
areBothEqual(
pres.spec.credentials[1].revealedAttributes, // 1 refers to second credential, which is credential2
{
credentialSubject: {
fname: 'John',
location: { country: 'USA' },
physical: { BMI: 23.25 }
}
}
)
);
// Verifier checks that the desired attribute are proved equal
console.assert(
areBothEqual(
pres4.spec.attributeEqualities,
[
[[0, 'credentialSubject.SSN'], [1, 'credentialSubject.sensitive.SSN']], // 0 refers to credential1, 1 refers to credential2
[[0, 'credentialSubject.city'], [1, 'credentialSubject.location.city']], // 0 refers to credential1, 1 refers to credential2
]
)
);
// Verifier checks that the desired attribute inequalities are enforced for credential1.
console.assert(
areBothEqual(
pres.spec.credentials[0].attributeInequalities, // 0 refers to credential1
{
credentialSubject: {
email: [
{ inEqualTo: '[email protected]', protocol: 'Uprove' },
{ inEqualTo: '[email protected]', protocol: 'Uprove' }
]
}
}
)
);
// Verify the cryptographic validity of the presentation
// Create a map of credential index -> public key to verify the presentation
const pks = new Map();
// Public key corresponding to credential1 is pk1 and credential1's index is 0
pks.set(0, pk1);
// Public key corresponding to credential2 is pk2 and credential2's index is 1
pks.set(1, pk2);
console.assert(pres.verify(pks).verified);
The holder can prove that certain attributes of the credential satisfy certain bounds, i.e. minimum and maximum. These are also called
range proofs. There are several supported protocols for enforcing bounds and each has different tradeoffs. Following examples describes
2 protocols, LegoGroth16 and Bulletproofs++. The former is a ZK-SNARK based protocol and has a trusted setup meaning that each verifier
has to do a ZK-SNARK setup (only once, and not per holder or per interaction) and then communicate the setup parameters, called proving key to the holder.
With Bulletproofs++, there is no trusted setup and hence the setup parameters can be generated by anyone. However LegoGroth16 is faster to verify (also has
other benefits like holder being able to reuse generated proofs but that is still present only in the Rust library).
Common setup for both bound check protocols
const jsonSchema1 = CredentialSchema.essential();
jsonSchema1.jsonSchema[SUBJECT_STR] = {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
email: { type: 'string' },
SSN: { $ref: '#/definitions/encryptableString' },
score: { type: 'number', minimum: -100, multipleOf: 0.1 },
}
};
jsonSchema1.properties['issuanceDate'] = { type: 'string', format: 'date' };
jsonSchema1.properties['expirationDate'] = { type: 'string', format: 'date' };
// Signer 1 with keys `sk1`, `pk1`, creates `credential1`
const builder1 = new BBSCredentialBuilder();
builder1.schema = credSchema1;
builder1.subject = {
fname: 'John',
lname: 'Smith',
email: '[email protected]',
SSN: '123-456789-0',
score: 55.5,
};
builder1.setTopLevelField('issuanceDate', '2023-09-14');
builder1.setTopLevelField('expirationDate', '2025-09-14');
const credential1 = builder1.sign(sk1);
Bound check using LegoGroth16
// Verifier does one time setup for ZK-SNARK and then shares the same setup parameters with each holder who wishes to create proofs
const pk = BoundCheckSnarkSetup();
// snarkProvingKey will be shared with the holder (prover) so compress it to make it shorter. This is not gzip like compression but EC point compression
const snarkProvingKey = pk.decompress();
// snarkVerifyingKey will be kept by the verifier to verifer the proof
const snarkVerifyingKey = pk.getVerifyingKeyUncompressed();
// Holder starts creating presentation
const presBuilder = new PresentationBuilder();
// PresentationBuilder assigns index 0 to credential1
console.assert(presBuilder.addCredential(credential1) === 0);
// Minimum and maximum value of attribute `expirationDate`
const [minExpDate, maxExpDate] = [new Date('2025-12-31'), new Date('2026-12-31')];
// Minimum and maximum value of attribute `credentialSubject.score`
const [minScore, maxScore] = [40, 85];
// paramId is used to let the PresentationBuilder uniquely identify the snarkProvingKey.
// If more than 1 bound check predicate is being proved then the next call to enforceBounds can omit passing snarkProvingKey and just pass paramId
const paramId = 'lg16';
// Enforce check on attribute expirationDate as minExpDate.toISOString() <= expirationDate < maxExpDate.toISOString()
presBuilder.enforceBounds(0, 'expirationDate', minExpDate.toISOString(), maxExpDate.toISOString(), paramId, snarkProvingKey);
// Enforce check on attribute credentialSubject.score as minScore <= credentialSubject.score < maxScore
presBuilder.enforceBounds(0, 'credentialSubject.score', minScore, maxScore, paramId);
const pres = presBuilder.finalize();
// The verifier checks if the correct bounds have been satisfied.
console.assert(
areBothEqual(pres.spec.credentials[0].bounds, {
credentialSubject: {
score: [{ // Note the array here. This is because multiple bounds can be proven on an attribute
min: minScore,
max: maxScore,
paramId: 'lg16',
protocol: 'LegoGroth16'
}]
},
expirationDate: [{
min: minExpDate,
max: maxExpDate,
paramId: 'lg16',
protocol: 'LegoGroth16'
}]
}
));
// Verifier passes the snark verification key for the presentation to verify
const pp = new Map();
// Verifier passes the same paramId
pp.set(paramId, snarkVerifyingKey);
const pks = new Map();
pks.set(0, pk1);
console.assert(pres.verify(pks, undefined, pp).verified);
Bound check using Bulletproofs++
// Holder starts creating presentation
const presBuilder = new PresentationBuilder();
// PresentationBuilder assigns index 0 to credential1
console.assert(presBuilder.addCredential(credential1) === 0);
// Minimum and maximum value of attribute `expirationDate`
const [minExpDate, maxExpDate] = [new Date('2025-12-31'), new Date('2026-12-31')];
// Minimum and maximum value of attribute `credentialSubject.score`
const [minScore, maxScore] = [40, 85];
// Note that no setup params are generated as they will be internally created if not passed.
// Enforce check on attribute expirationDate as minExpDate.toISOString() <= expirationDate < maxExpDate.toISOString()
presBuilder.enforceBounds(0, 'expirationDate', minExpDate.toISOString(), maxExpDate.toISOString());
// Enforce check on attribute credentialSubject.score as minScore <= credentialSubject.score < maxScore
presBuilder.enforceBounds(0, 'credentialSubject.score', minScore, maxScore);
const pres = presBuilder.finalize();
// The verifier checks if the correct bounds have been satisfied.
console.assert(
areBothEqual(pres.spec.credentials[0].bounds, {
credentialSubject: {
score: [{ // Note the array here. This is because multiple bounds can be proven on an attribute
min: minScore,
max: maxScore,
protocol: 'Bulletproofs++'
}]
},
expirationDate: [{
min: minExpDate,
max: maxExpDate,
protocol: 'Bulletproofs++'
}]
}
)
);
const pks = new Map();
pks.set(0, pk1);
console.assert(pres.verify(pks).verified);
With verifiable encryption, a verifier should be able to check that the holder encrypted certain attributes from his credential
for a 3rd party, say a regulator/auditor, which can decrypt those but not the verifier. This is done using a ZK-SNARK based
protocol called SAVER. Here the 3rd party, the decryptor, does a ZK-SNARK setup, publishes the setup parameters which
are used by the holder and verifier respectively. The holder can encrypt any attributes for any number of decryptors. each having
separate keys
// This uses credential1 from previous examples which has a field called SSN which will be encrypted by the holder
// Setup done by the 3rd party (decryptor)
const chunkBitSize = 16;
const encGens = dockSaverEncryptionGens();
const [saverSnarkPk, saverSec, encryptionKey, decryptionKey] = SaverDecryptor.setup(encGens, chunkBitSize);
const saverSk = saverSec;
// Needed by the holder (prover)
const saverProvingKey = saverSnarkPk.decompress();
// Needed by the verifier
const saverVerifyingKey = saverSnarkPk.getVerifyingKeyUncompressed();
// Needed by both, the holder and verifier
const saverEk = encryptionKey.decompress();
// Needed by 3rd party to decrypt and verifier to verify the decryption result
const saverDk = decryptionKey.decompress();
// Setup done by the verifier and shared with the holder
const ck = SaverChunkedCommitmentKey.generate(stringToBytes('some nonce'));
const commKey = ck.decompress();
// To identify different parameters
const commKeyId = 'random-1';
const ekId = 'random-2';
const snarkPkId = 'random-3';
// Holder starts creating presentation
const presBuilder = new PresentationBuilder();
// PresentationBuilder assigns index 0 to credential1
console.assert(presBuilder.addCredential(credential1) === 0);
presBuilder.verifiablyEncrypt(
0,
'credentialSubject.SSN',
chunkBitSize,
commKeyId,
ekId,
snarkPkId,
commKey,
saverEk,
saverProvingKey
);
const pres = presBuilder.finalize();
// Verifier checks that the correct encryption key and other parameters were used by the prover
console.assert(
areBothEqual(pres.spec.credentials[0].verifiableEncryptions, {
credentialSubject: {
SSN: [{ // Note the array here. This is because multiple encryption can be done on an attribute
chunkBitSize,
commitmentGensId: commKeyId,
encryptionKeyId: ekId,
snarkKeyId: snarkPkId,
protocol: 'SAVER'
}]
}
})
);
// These checks are made by the verifier, i.e. verifier checks that the ciphertext for each required attribute is
// present in the presentation and the presentation is valid. The verifier will preserve the ciphertext to be later
// passed on to the decryptor
// @ts-ignore
console.assert(pres1.attributeCiphertexts.size === 1);
// Verifier passes the snark verification key and other params for the presentation to verify
const pp = new Map();
pp.set(commKeyId, commKey);
pp.set(ekId, saverEk);
pp.set(snarkPkId, saverVerifyingKey);
console.assert(pres.verify(pks, undefined, pp).verified);
// Verifier extracts the ciphertext from the presentation
const ciphertexts = pres.attributeCiphertexts?.get(0);
// Decryptor gets the ciphertext from the verifier and decrypts it
let cts = _.get(ciphertexts, 'credentialSubject.SSN') as (SaverCiphertext | SaverCiphertext[]); // Get the ciphertext for attribute credentialSubject.SSN
if (!Array.isArray(cts)) { // As the holder might have encrypted for multiple decryptors
cts = [cts];
}
cts.forEach((ciphertext) => {
const decrypted = SaverDecryptor.decryptCiphertext(ciphertext, saverSk, saverDk, saverVerifyingKey, chunkBitSize);
// The decrypted message is raw bytes which can be decoded to get the attribute string.
console.assert(MessageEncoder.reversibleDecodeStringForSigning(decrypted.message) === _.get(credential1.subject, 'SSN'));
// Decryptor shares the decryption result with verifier which the verifier can check for correctness.
console.assert(
ciphertext.verifyDecryption(
decrypted,
saverDk,
saverVerifyingKey,
dockSaverEncryptionGensUncompressed(),
chunkBitSize
).verified
);
});
For credential revocation, the holder uses accumulator where it can prove membership/non-membership depending on how the revocation is implemented.
Assuming a credential is revocable, i.e. signer called setCredentialStatus
while signing, following example shows how to use it during presentations.
const builder = new BBSCredentialBuilder();
// ..
builder.subject = {
// .... attributes
};
builder.setCredentialStatus('dock:accumulator:accumId123', MEM_CHECK_STR, 'user:A-123', RevocationStatusProtocol.Vb22);
const credential = builder.sign(sk);
const presBuilder = new PresentationBuilder();
console.assert(presBuilder.addCredential(credential) === 0);
// Any other predicates ....
// Holder specifying accumulator witness and value for status of `credential`
presBuilder.addAccumInfoForCredStatus(
0, // Refers to credential index for whose status this accumulator is being used
accumulatorWitness, // Witness of the accumulator
accumulator.accumulated, // Accumulator value for which the above witness is valid
accumulatorPk, // Public key of the accumulator
{ // This is optional information and can be used to indicate timestamp of the accumulator being used, here it refers to the block number (of the blockchain where the accumulator is hosted) corresponding to the accumulator value.
blockNo: 2010334
}
);
const pres = presBuilder.finalize();
// This check is made by the verifier, i.e. verifier checks that the accumulator id, type, value and timestamp (`blockNo`)
// are as expected
console.assert(areBothEqual(pres.spec.getStatus(0), {
id: 'dock:accumulator:accumId123',
[TYPE_STR]: VB_ACCUMULATOR_22,
revocationCheck: MEM_CHECK_STR,
accumulated: accumulator.accumulated,
extra: { blockNo: 2010334 }
}));
const acc = new Map();
acc.set(0, accumulatorPk);
console.assert(pres.verify([pk], acc).verified);
For more complex predicates, they can be specified using Circom (see the main Readme for the Circom workflow, R1CS files, WASM files, etc).
const builder = new BBSCredentialBuilder();
// ..
builder.subject = {
// .... attributes
};
const credential = builder.sign(sk);
const presBuilder = new PresentationBuilder();
console.assert(presBuilder.addCredential(credential) === 0);
presBuilder.enforceCircomPredicate(
0, // Refers to credential index 0
[['x', 'credentialSubject.education.grade']], // An array of pairs for private variables of the circuit. The first item of pair the variable name and second is the attribute it corresponds to.
[['set', publicValue]], // An array of pairs for public variables of the circuit. The first item of pair the variable name and second is the value it corresponds to.
circuitId, // Identifier of the circuit. Useful as multiple circuits can be used in a presentation or same circuit can be used for multiple predicates
pkId, // Identifier of the proving key for the circuit
r1cs,
wasm,
provingKey
);
// Above uses a single circuit for predicates over a single credential. Its possible to use a single circuit with several credentials,
// i.e. some variable of circuit correspond to one credential, some to another and so using `enforceCircomPredicateAcrossMultipleCredentials`
const pres = presBuilder.finalize();
// Verifier should check that the spec has the required predicates and also check the variable names are mapped
// to the correct attributes or public values for private and public variables respectively
console.assert(pres.spec.credentials[0].circomPredicates?.length === 1);
console.assert(pres.spec.credentials[0].circomPredicates[0].privateVars.length === 1);
console.assert(areBothEqual(pres.spec.credentials[0].circomPredicates[0].privateVars[0], {
varName: 'x',
attributeName: { credentialSubject: { education: { grade: null } } }
}
));
console.assert(pres.spec.credentials[0].circomPredicates[0].publicVars.length === 1);
console.assert(pres.spec.credentials[0].circomPredicates[0].publicVars[0].varName === 'set');
console.assert(areBothEqual(pres.spec.credentials[0].circomPredicates[0].publicVars[0].value, publicValue));
const pp = new Map();
pp.set(pkId, verifyingKey);
pp.set(PresentationBuilder.r1csParamId(circuitId), getR1CS(r1cs));
pp.set(PresentationBuilder.wasmParamId(circuitId), wasm);
// Set output variable for circuit.
const circomOutputs = new Map();
circomOutputs.set(0, [[publicValue]]);
console.assert(pres.verify([pk], undefined, pp, circomOutputs).verified);
See these tests for examples of presentation creation, verification and (de)serialization with use of the above-mentioned features.
A user/holder can request a blinded credential from the signer/issuer where some of the attributes are not known to the signer. The blinded
credential needs to be then unblinded, i.e. converted to a normal credential which can be verified by the signer's public key and used in presentations.
The workflow to do this is as follows: user uses a BlindedCredentialRequestBuilder to create BlindedCredentialRequest which is sent
to the signer and subsequently verified by the signer. On successful verification, the signer uses BlindedCredentialRequest
to create
a BlindedCredentialBuilder to build a BlindedCredential. The BlindedCredential
is sent to the user who then converts it to a
normal credential. The BlindedCredentialRequest
contains a Presentation
inside which is verified by the signer.
The user while requesting such a credential might need to prove the possession of other credentials or prove that some of the
blinded (hidden) attributes are same as the credentials in the presentation. The user can do this by calling methods
on BlindedCredentialRequestBuilder
, eg, calling addCredentialToPresentation
will add a Credential
already possessed by
the user to the Presentation
contained in the BlindedCredentialRequest
, enforceBoundsOnCredentialAttribute
will enforce bounds (min, max)
on the credential attribute, etc. Any predicate supported in Presentation
s can be proved over the credential added in BlindedCredentialRequest
.
Predicates can also be proven over the blinded attributes, eg, markBlindedAttributesEqual
can be used to prove some blinded attribute equal to a
credential attribute, verifiablyEncryptBlindedAttribute
can be used to verifiably encrypt a blinded attribute, etc.
Holder requesting a credential with some attributes blinded and proves some predicates about the blinded attributes
// Holder and issuer should know the schema for credential
const schema = CredentialSchema.essential();
schema.properties[credentialSubject] = {
type: 'object',
properties: {
fname: { type: 'string' },
lname: { type: 'string' },
sensitive: {
type: 'object',
properties: {
email: { type: 'string' },
phone: { type: 'string' },
SSN: { $ref: '#/definitions/encryptableString' }
}
},
education: {
type: 'object',
properties: {
studentId: { type: 'string' },
university: {
type: 'object',
properties: {
name: { type: 'string' },
registrationNumber: { type: 'string' }
}
},
transcript: {
type: 'object',
properties: {
rank: { type: 'integer', minimum: 0 },
CGPA: { type: 'number', minimum: 0, multipleOf: 0.01 },
scores: {
type: 'object',
properties: {
english: { type: 'integer', minimum: 0 },
mathematics: { type: 'integer', minimum: 0 },
science: { type: 'integer', minimum: 0 },
history: { type: 'integer', minimum: 0 },
geography: { type: 'integer', minimum: 0 }
}
}
}
}
}
}
}
};
const credSchema = new CredentialSchema(schema);
// These are the attributes holder wants to hide from issuer
const blindedSubject = {
sensitive: {
email: '[email protected]',
SSN: '123-456789-0'
},
education: {
studentId: 's-22-123450',
university: {
registrationNumber: 'XYZ-123-789'
}
}
};
// Holder creates the request to get a blinded credential
const reqBuilder = new BBSBlindedCredentialRequestBuilder();
reqBuilder.schema = schema;
reqBuilder.subjectToBlind = blindedSubject;
// Holder proves some predicates about the blinded attributes. These will be demanded by the issuer
reqBuilder.enforceInequalityOnBlindedAttribute('credentialSubject.sensitive.email', '[email protected]');
reqBuilder.enforceInequalityOnBlindedAttribute('credentialSubject.sensitive.SSN', '1234');
// Finalize the builder to get the blinded credential request. Note that in case of BBS+ or BDDT16, `finalize` will
// return a blinding as well which will be used to unblind the blinded credential to get a normal credential. This
// request is sent to the issuer
const blindedCredRequest = reqBuilder.finalize();
// Issuer checks that the desired conditions have been enforced.
console.assert(areBothEqual(blindedCredRequest.presentation.spec.blindCredentialRequest.attributeInequalities, {
credentialSubject: {
sensitive: {
email: [
{ inEqualTo: '[email protected]', protocol: 'Uprove' },
],
SSN: [{ inEqualTo: '1234', protocol: 'Uprove' }]
}
}
})
);
// Issuer checks the cryptographic validity of the request. Note the empty map. This is because while requesting the blinded credential,
// the user is not presenting other credentials as well. This will be seen in next example
console.assert(blindedCredRequest.verify(new Map()).verified);
// Issuer start building the blinded credential
const blindedCredBuilder = req.generateBlindedCredentialBuilder();
// Issuer knows these attributes. These are called unblinded attributes
blindedCredBuilder.subject = {
fname: 'John',
lname: 'Smith',
education: {
university: {
name: 'Example University'
},
transcript: {
rank: 100,
CGPA: 2.57,
scores: {
english: 60,
mathematics: 70,
science: 50,
history: 45,
geography: 40
}
}
}
};
// Issuer finally signs to create the blinded credential. This will be sent to the holder
const blindedCred = blindedCredBuilder.sign(sk);
// Holder creates a normal credential from the blinded credential. For BBS+ or BBDT16, holder would have used the blinding
// returned during `reqBuilder.finalize` and passed to `blindedCred.toCredential`
const credential1 = blindedCred.toCredential(blindedSubject);
// This credential can be verified with signer's public key
credential1.verify(pk)
An issuer might demand a presentation from another credential before issuing a blinded credential. Eg. before issuing blinded
credential credential2
, issuer needs a presentation from credential1
(along with any predicates). Following shows that
holder while requesting a blinded credential, shares a presentation from another credential, credential1
and also proves that certain
attributes of credential1
and the blinded credential credential2
are same.
// The holder already has credential1 from the previous example
const blindedSubject = {
email: '[email protected]',
SSN: '123-456789-0',
userId: 'user:123-xyz-#',
secret: 'my-secret-that-wont-tell-anyone'
};
// Holder creates the request to get a blinded credential
const reqBuilder = new BBSBlindedCredentialRequestBuilder();
reqBuilder.schema = schema2; // some schema for new credential2
reqBuilder.subjectToBlind = blindedSubject;
// Holder shares a presentation of a credential along with blinded credential request. Any number of credential could be added
console.assert(reqBuilder.addCredentialToPresentation(credential1) === 0);
// Reveal some attributes from credentail1
reqBuilder.markCredentialAttributesRevealed(
0,
new Set<string>([
'credentialSubject.education.university.name',
'credentialSubject.education.university.registrationNumber'
])
);
// Prove that credentialSubject.SSN attribute of credentail1 is same as the credentialSubject.sensitive.SSN blinded attribute
reqBuilder.enforceEqualityOnBlindedAttribute(
[
'credentialSubject.SSN',
[
[0, 'credentialSubject.sensitive.SSN'] // 0 refers to credential1
]
]
);
// Prove that credentialSubject.email attribute of credentail1 is same as the credentialSubject.sensitive.email blinded attribute
reqBuilder.enforceEqualityOnBlindedAttribute(
[
'credentialSubject.email',
[
[0, 'credentialSubject.sensitive.email'] // 0 refers to credential1
]
]
);
// More predicates could be satisfied on credential1 as
// reqBuilder.enforceBoundsOnCredentialAttribute(0, 'credentialSubject.education.transcript.CGPA', minCGPA, maxCGPA);
// ...
const blindedCredRequest = reqBuilder.finalize();
const pks = new Map();
pks.set(0, pk1);
console.assert(blindedCredRequest.verify(pks).verified);
// Issuer start building the blinded credential
const blindedCredBuilder = req.generateBlindedCredentialBuilder();
// Issuer sets the known attributes, creates the blinded credential and holder unblinds the blinded credential, as in the example before
See these tests for examples of using these predicates.
KVAC stands for Keyed-Verification Anonymous Credentials. Verifying them requires the secret key of the signer (issuer) or a proof
given by the signer unlike regular credentials where signer's public key is required. For presentations created from KVAC,
they can't be "fully" verified without the signer's secret key. These presentations can be considered to be composed of 2 parts,
one which can be verified without the secret key and the other which needs the secret key. The latter is what we call KeyedProof
as it will be keyed to the signer to verify. The expected usage of KVAC presentations is for the verifier to verify the part
that does not require the usage of secret key and send the other part to the signer to verify thus making the verifications always
require the signer. This is useful when the signer wants to be aware of anytime its issued credential is used, eg. charging for it.
Note that the KeyedProof
does not contain any revealed attributes or predicates or unique ids so the signer cannot
learn any identifying information from it. Not all kinds of credentials support this capability and currently only
BBDT16Credential supports it. Note that it does not support the verify
method which expects signer's
public key but instead supports verifyUsingValidityProof
method which requires a BBDT16MacProofOfValidity
that can be created by the signer. Unlike regular credentials, BBDT16Credential
contain a MAC
and not a signature which require the secret key for verification.
In cases where the verifier does not trust the signer to communicate the result of verification of KeyedProof
honestly, i.e. it suspects
that the signer might report KeyedProof
to be invalid when its valid or vice versa. In that case, the verifier can request the signer to
provide a proof of validity or proof of invalidity of the KeyedProof
by calling proofOfValidity
or proofOfInvalidity
This principle also applies to credential revocation (status
field) where the revocation status can only be checked by the issuer.
const params = BBDT16MacParams.generate(1, BBDT16_MAC_PARAMS_LABEL_BYTES);
// Credential signature works as with other schemes
const credential1 = builder.sign(sk);
// As KVAC credentials can't be direcly verified using the public key, signer creates a proof of validity of credential and gives to the holder
const proof = credential1.proofOfValidity(sk, pk, params);
// Holder verifies the proof to be assured of the credential validity
console.assert(credential1.verifyUsingValidityProof(proof, pk, params).verified);
// Holder creates presentation as usual, adds predicates, etc as usual
const presBuilder = new PresentationBuilder();
// PresentationBuilder assigns index 0 to credential1
console.assert(presBuilder.addCredential(credential1) === 0);
// ....
const pres = presBuilder.finalize();
// Now there are 2 possibilities, either the verifier is same as the signer (or verifier knows the secret key) or
// verifier extracts KeyedProof from the presentation and takes it to the signer who can then verify it using the secret key.
// Verifier is same as the signer
const pks = new Map();
pks.set(0, sk); // Notice its passing the secret key for verification
console.assert(pres.verify(pks).verified);
// Verifier doesn't have secret key and extracts the KeyedProof
const keyedProofs = pres.getKeyedProofs();
// Get KeyedProof for credential at index 0
const keyedCredProof = keyedProofs.get(0);
// Now signer uses his secret key to verify the keyed proof
console.assert(keyedCredProof?.credential?.sigType === 'Bls12381BBDT16MACDock2024');
console.assert(keyedCredProof?.verify(sk).verified);
// Signer create a proof of validity of the keyed-proof and gives to the verifier
const pv = proof.proofOfValidity(sk, pk, params);
// Verifier checks the proof and is convinced that the signer reported the verification result correctly
console.assert(pv.verify(proof, pk, params).verified);
// KeyedProof can be serialized/deserialized from JSON.
let j = keyedCredProof.toJSON();
let recreated = KeyedProof.fromJSON(j);
See these tests for examples.
Note that lot of classes mentioned above are abstract as this project supports multiple signature schemes.
- Accumulator usage in general, i.e. without using credentialStatus field.