`cmz` by Ian Goldberg, iang@uwaterloo.ca
Version 0.1.0, 2025-10-10
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](https://docs.rs/group/0.13.0/group/prime/trait.PrimeGroup.html).
A typical such group would be
[curve25519\_dalek::ristretto::RistrettoPoint](https://docs.rs/curve25519-dalek/4.1.3/curve25519_dalek/ristretto/struct.RistrettoPoint.html).
To declare a credential type, use the `CMZ!` macro at the top level of
your crate or module (outside of any function):
```text
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`).
## CMZ Protocols
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).
### Example
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):
```text
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.
```text
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 `prepare` function, passing it the two
credentials to be shown, as well as a partially constructed
credential to be issued
- The `prepare` function will output a `Request` struct, and a
`ClientState` struct.
- The client will send the `Request` struct to the issuer. (The
struct has serialization and deserialization methods.)
- The issuer will call the `handle` function, 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 a `Reply` struct.
- The issuer will send the `Reply` struct to the client. (Again it
has serialization and deserialization methods.)
- The client will pass the `Reply` struct to the `finalize` method of
the `ClientState` struct it held on to. If everything goes well,
the `finalize` method will output the completed newly issued
credential.
### API
The generated `wallet_spend::prepare` function (run by the client) has
the following signature:
```text
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:
```text
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:
```text
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.
### Parameterized Protocols
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:
```text
muCMZProtocol! { proto_name,
```
to:
```text
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:
```text
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 Params` declared in the generated module,
containing a `Scalar` field for each named parameter.
- The `prepare` function takes an additional `&Params` argument at the
end, which the client must fill in before calling `prepare`.
- The `fill_creds` callback returns `Result<Params,CMZError>` instead of
`Result<(),CMZError>`, and it is the job of that callback to supply
a filled-in `Params` struct, possibly based on other values it
receives from the client in the attributes of the credentials.