A crate to automatically create protocols that use CMZ14 or µCMZ credentials, by specifying an extremely compact description of the protocol.
|
2 hónapja | |
---|---|---|
cmzcred_derive | 2 hónapja | |
src | 3 hónapja | |
tests | 2 hónapja | |
Cargo.toml | 3 hónapja | |
README.md | 2 hónapja |
This crate is centred around the concept of credentials. A credential contains:
Scalar
)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:
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:
Each credential specification list can be:
Each credential specification is:
CMZ!
macroCMZ!
macro), annotated with the attribute specificationAn attribute specification for a credential to be shown is one of:
An attribute specification for a credential to be issued is one of:
For the attributes:
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:
prepare
function, passing it the two
credentials to be shown, as well as a partially constructed
credential to be issuedprepare
function will output a Request
struct, and a
ClientState
struct.Request
struct to the issuer. (The
struct has serialization and deserialization methods.)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.Reply
struct to the client. (Again it
has serialization and deserialization methods.)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.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:
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:
struct Params
declared in the generated module,
containing a Scalar
field for each named parameter.prepare
function takes an additional &Params
argument at the
end, which the client must fill in before calling prepare
.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.