cmz by Ian Goldberg, [email protected]
Version 0.1.0-rc2, 2025-09-22
This crate is centred around the concept of credentials. A credential contains:
- A number of attributes (each of a type called
Scalar) - A Message Authentication Code (MAC), which is two values of type
Point
Credentials are held by clients, and are both issued and validated by an issuer. With CMZ credentials (the kind used in this crate), the issuer is the only entity that can check whether a given credential is valid. (Checking the credential requires the same secret key as is used to create the credential.)
Your application can have multiple different kinds of credentials, each
with its own set of attributes. All of the credentials in your
application should use the same Scalar and Point types. You get
these from a mathematical group, which must satisfy the trait
group::prime::PrimeGroup.
A typical such group would be
curve25519_dalek::ristretto::RistrettoPoint.
To declare a credential type, use the CMZ! macro at the top level of
your crate or module (outside of any function):
CMZ! { Lox<RistrettoPoint> :
id,
bucket,
trust_level,
level_since,
invites_remaining,
blockages
}
This declares a credential type called Lox using the mathematical
group RistrettoPoint. The credential has six attributes, with the
names id, bucket, etc.
If you omit the <RistrettoPoint>, a default of <G> will be assumed,
so you will need to have a group called G in scope. For example:
use curve25519_dalek::ristretto::RistrettoPoint as G;
Note that this macro declares a type for a credential. Your application may have any number (zero or more) actual credentials of this type.
The attribute fields of this credential are of type Option<Scalar>.
The field values could be None if, for example, a credential is
incomplete (in the process of being issued, and the attributes are not
fully filled in yet), or if an attribute is being hidden from the issuer
(in which case the issuer will see a credential with some of the fields
being None).
A protocol is executed by a client and the issuer, and involves:
- Proving possession of ("showing") zero or more credentials, which may be of the same or different credential types
- Requesting zero or more new credentials to be issued, which may be of the same or different credential types
Importantly, when a client shows a credential and/or requests for a new credential to be issued, the attributes of those credentials are not necessarily revealed to the issuer. The protocol defines which attributes are revealed, and which are hidden. (There are also a few more options, described below.) For the attributes that are hidden, the client can nonetheless prove that certain facts about them are true, using a zero-knowledge proof (which will be automatically created and checked by the modules generated by this crate).
Suppose we have a credential type called Wallet, with two attributes
randid (a random id number for the wallet) and balance (the amount
of funds in the wallet). We also have a second credential type called
Item, representing items that can be purchased, with two attributes
serialno (the serial number of the item), and price (the price of
the item):
CMZ! { Wallet: randid, balance }
CMZ! { Item: serialno, price }
Now we want to implement a zero-knowledge protocol by which a client who holds a wallet with a given balance can buy an item and be issued a new wallet with the remaining balance. The balance, however, is not revealed to the issuer. To avoid double-spending (using an old wallet with a larger balance after having spent some of that balance already), the random id of the wallet will be revealed in each transaction, and the issuer will reject attempts to use the same random id two or more times. The new wallet will be created with a fresh random id that is also unknown to the issuer, so that the issuer cannot track clients from transaction to transaction. Items for purchase are represented by credentials that anyone can download from the issuer's website.
The primary way to create a protocol is with the muCMZProtocol! macro.
muCMZProtocol! { wallet_spend,
[ W: Wallet { randid: R, balance: H },
I: Item { serialno: H, price: H } ],
N: Wallet { randid: J, balance: H },
(0..=100000000).contains(N.balance),
W.balance = N.balance + I.price
}
The parameters to the macro call are:
- an identifier for the protocol
- a list of zero or more specifications for credentials that will be shown
- a list of zero or more specifications for credentials that will be issued
- zero or more statements relating the attributes in the credentials
Each credential specification list can be:
- empty
- a single credential specification
- a square-bracketed list of credential specifications
Each credential specification is:
- an identifier for the credential
- a type for the credential, previously defined with the
CMZ!macro - a braced list of the attributes of the credential (as defined in
the
CMZ!macro), annotated with the attribute specification
An attribute specification for a credential to be shown is one of:
- H (hide)
- R (reveal)
- I (implicit)
An attribute specification for a credential to be issued is one of:
- H (hide)
- R (reveal)
- I (implicit)
- S (set by issuer)
- J (joint creation)
For the attributes:
- "hide" means that the attribute is not revealed to the issuer (but the statements may still prove things about them).
- "reveal" means that the attribute is revealed to the issuer.
- "implicit" means that some other part of the overall system means that both the client and the issuer already know what the value of this attribute should be, and so it doesn't need to be sent in the CMZ protocol (saving some space).
- "set by issuer", for an attribute in a credential to be issued, means that the issuer will choose the value of this attribute, and send it back to the client with the issued credential.
- "joint creation" means that both the client and the issuer will contribute a random component to this attribute; the resulting attribute will be the sum of those components. The issuer will have no information about the resulting attribute value, and the client will not be able to predict the resulting attribute value before receiving the newly issued credential.
So in the example, we are creating a protocol called wallet_spend,
where the client needs to already have two credentials (their current
Wallet W and the credential I for the item they wish to purchase). The
client will receive back a new Wallet credential N. (Outside of this
protocol, the issuer would likely send the item being purchased to the
client, perhaps using Private Information Retrieval, or something like
that, since the item's serial number and price are hidden from the
issuer in this example protocol.)
This macro invocation creates a module called wallet_spend that
contains definitions of three structs and two functions. The general
flow is:
- The client calls the
preparefunction, passing it the two credentials to be shown, as well as a partially constructed credential to be issued - The
preparefunction will output aRequeststruct, and aClientStatestruct. - The client will send the
Requeststruct to the issuer. (The struct has serialization and deserialization methods.) - The issuer will call the
handlefunction, which, if everything checks out, will output the two shown credentials and the newly issued credential, with only the attributes visible to the issuer filled in. It will also output aReplystruct. - The issuer will send the
Replystruct to the client. (Again it has serialization and deserialization methods.) - The client will pass the
Replystruct to thefinalizemethod of theClientStatestruct it held on to. If everything goes well, thefinalizemethod will output the completed newly issued credential.
The generated wallet_spend::prepare function (run by the client) has
the following signature:
pub fn prepare(
rng: &mut impl RngCore,
session_id: &[u8],
W: &Wallet,
I: &Item,
N: Wallet,
) -> Result<(Request, ClientState), CMZError>
The session_id parameter is a session identifier. It can be any
sequence of bytes, but the value passed here to prepare and below to
handle must be the same.
You should treat the Request and ClientState structs as opaque, but
they are currently not, and have Debug implemented, so if you wanted,
you could look inside with println!("{:#?}", request) or similar.
You can serialize and deserialize a Request struct with
request.as_bytes() and wallet_spend::Request::try_from(bytes), or
using serde (Serialize and Deserialize are implemented for
Request.)
The generated wallet_spend::handle function (run by the issuer) has
the following signature:
pub fn handle<F,A>(
rng: &mut impl RngCore,
session_id: &[u8],
request: Request,
fill_creds: F,
authorize: A,
) -> Result<(Reply, (Wallet, Item, Wallet)), CMZError>
where
F: FnOnce(&mut Wallet, &mut Item, &mut Wallet) -> Result<(),CMZError>,
A: FnOnce(&Wallet, &Item, &Wallet) -> Result<(),CMZError>
Note that handle consumes the Request.
The handle function takes two callbacks: fill_creds and authorize.
The handle function will read the request, and use it to fill in
the revealed attributes from the shown and issued credentials (in this case,
just W.randid). The hidden attributes from the credentials will be
set to None, as will the implicit, set by issuer, and joint creation
attributes. It is the job of the fill_creds callback to:
- Set the values of the implicit and set by issuer attributes (if any) for each shown and issued credential
- Set the private keys for each credential
The handle function will then check that the credentials shown by the
client are all valid, and that the statements given in the
muCMZProtocol! macro call are all true. If not, it will return with an
Err. If so, handle will call the authorize callback, which can do
any final application-specific checks on the credentials (and any other
state it can access in its closure). If authorize returns Err, so
will handle. If authorize returns Ok, then handle will issue
the credentials to be issued (in this case, the new Wallet credential).
It will return a Reply struct and copies of the shown and issued
credentials (but the attributes not visible to the issuer will still be
None of course).
The Reply struct can be serialized and deserialized in the same way as
the Request struct, so that it can be sent back to the client.
The client will then pass that deserialized Reply struct into the
finalize method of the ClientState struct that was output by
prepare, above. The finalize method has the following signature:
pub fn finalize(
self,
reply: Reply,
) -> Result<Wallet, (CMZError, Self)>
Note that finalize consumes both the Reply and also self. In
the event of an error (such as a malicious reply impersonating the
issuer?), self is returned so you can possibly try again. In the
event of success, the newly issued credentials are returned as a tuple,
or if as in this case, there's just one, as a single element.
A protocol can optionally be declared as having parameters, which are
public Scalar or Point constants that will be filled in at runtime. You
declare parameters by changing the first line of the muCMZProtocol!
macro invocation from, for example:
muCMZProtocol! { proto_name,
to:
muCMZProtocol! { proto_name<param1, param2, @param3>,
then you can use param1 and param2 wherever you could have used a
literal Scalar constant in the statements in the statement list, and
param3 wherever you could have used a public Point (the @ indicates
the parameter is a Point; the default is that the parameter is a
Scalar). For example:
muCMZProtocol! { wallet_spend<fee>,
[ W: Wallet { randid: R, balance: H },
I: Item { serialno: H, price: H } ],
N: Wallet { randid: J, balance: H },
(0..=100000000).contains(N.balance),
W.balance = N.balance + I.price + fee
}
If you declare parameters in your protocol, the API changes as follows:
- There is a
struct Paramsdeclared in the generated module, containing aScalarfield for each named parameter. - The
preparefunction takes an additional&Paramsargument at the end, which the client must fill in before callingprepare. - The
fill_credscallback returnsResult<Params,CMZError>instead ofResult<(),CMZError>, and it is the job of that callback to supply a filled-inParamsstruct, possibly based on other values it receives from the client in the attributes of the credentials.