A crate to automatically create protocols that use CMZ14 or µCMZ credentials, by specifying an extremely compact description of the protocol.

Ian Goldberg 32697bba53 Support CMZ protocols with public Points as parameters, not just public Scalars 2 달 전
cmzcred_derive 32697bba53 Support CMZ protocols with public Points as parameters, not just public Scalars 2 달 전
src e7b44882fc Ensure the rng implements CryptoRng + RngCore 3 달 전
tests a90bde0b2c cargo fmt 2 달 전
Cargo.toml e7b44882fc Ensure the rng implements CryptoRng + RngCore 3 달 전
README.md 32697bba53 Support CMZ protocols with public Points as parameters, not just public Scalars 2 달 전

README.md

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).

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):

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 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:

    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, set by issuer, and the issuer contribution to joint creation 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.

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:

    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 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.