| Internet-Draft | ARC Cryptography | February 2026 |
| Yun, et al. | Expires 25 August 2026 | [Page] |
This document specifies the Anonymous Rate-Limited Credential (ARC) protocol, a specialization of keyed-verification anonymous credentials with support for rate limiting. ARC credentials can be presented from client to server up to some fixed number of times, where each presentation is cryptographically bound to client secrets and application-specific public information, such that each presentation is unlinkable from the others as well as the original credential creation. ARC is useful in applications where a server needs to throttle or rate-limit access from anonymous clients.¶
This note is to be removed before publishing as an RFC.¶
The latest revision of this draft can be found at https://ietf-wg-privacypass.github.io/draft-arc/draft-ietf-privacypass-arc-crypto.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-ietf-privacypass-arc-crypto/.¶
Discussion of this document takes place on the PRIVACYPASS Privacy Pass mailing list (mailto:privacy-pass@ietf.org), which is archived at https://mailarchive.ietf.org/arch/browse/privacy-pass. Subscribe at https://www.ietf.org/mailman/listinfo/privacy-pass/.¶
Source for this draft and an issue tracker can be found at https://github.com/ietf-wg-privacypass/draft-arc.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 25 August 2026.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
This document specifies the Anonymous Rate-Limited Credential (ARC) protocol, a specialization of keyed-verification anonymous credentials with support for rate limiting.¶
ARC is privately verifiable (keyed-verification), yet differs from similar token-based protocols in that each credential can be presented multiple times without violating unlinkability of different presentations. Servers issue credentials to clients that are cryptographically bound to client secrets and some public information. Afterwards, clients can present this credential to the server up to some fixed number of times, where each presentation provides proof that it was derived from a valid (previously issued) credential and bound to some public information. Each presentation is pairwise unlinkable, meaning the server cannot link any two presentations to the same client credential, nor can the server link a presentation to the preceding credential issuance flow. Notably, the maximum number of presentations from a credential is fixed by the application.¶
ARC is useful in settings where applications require a fixed number of zero-knowledge proofs about client secrets that can also be cryptographically bound to some public information. This capability lets servers use credentials in applications that need throttled or rate-limited access from anonymous clients.¶
The following functions and notation are used throughout the document.¶
concat(x0, ..., xN): Concatenation of byte strings. For example, concat(0x01, 0x0203, 0x040506) = 0x010203040506.¶
bytes_to_int and int_to_bytes: Convert a byte string to and from a non-negative integer. bytes_to_int and int_to_bytes are implemented as OS2IP and I2OSP as described in [RFC8017], respectively. Note that these functions operate on byte strings in big-endian byte order.¶
random_integer_uniform(M, N): Generate a random, uniformly distributed integer R between M inclusive and N exclusive, i.e., M <= R < N.¶
random_integer_uniform_excluding_set(M, N, S): Generate a random, uniformly distributed integer R between M inclusive and N exclusive, i.e., M <= R < N, such that R does not exist in the set of integers S.¶
All algorithms and procedures described in this document are laid out in a Python-like pseudocode. Each function takes a set of inputs and parameters and produces a set of output values. Parameters become constant values once the protocol variant and the ciphersuite are fixed.¶
The notation T U[N] refers to an array called U containing N items of type
T. The type opaque means one single byte of uninterpreted data. Items of
the array are zero-indexed and referred as U[j] such that 0 <= j < N.
The notation {T} refers to a set consisting of elements of type T.
For any object x, we write len(x) to denote its length in bytes.¶
String values such as "CredentialRequest", "CredentialResponse", "Presentation", and "Tag" are ASCII string literals.¶
The following terms are used throughout this document.¶
Client: Protocol initiator. Creates a credential request, and uses the corresponding server response to make a credential. The client can make multiple presentations of this credential.¶
Server: Computes a response to a credential request, with its server private keys. Later the server can verify the client's presentations with its private keys. Learns nothing about the client's secret attributes, and cannot link a client's request/response and presentation steps.¶
The construction in this document has one primary dependency:¶
Group: A prime-order group implementing the API described below in Section 3.1.
See Section 6 for specific instances of groups.¶
In this document, we assume the construction of an additive, prime-order
group Group for performing all mathematical operations. In prime-order groups,
any element (other than the identity) can generate the other elements of the
group. Usually, one element is fixed and defined as the group generator.
In the ARC setting, there are two fixed generator elements (generatorG, generatorH).
Such groups are uniquely determined by the choice of the prime p that defines the
order of the group. (There may, however, exist different representations
of the group for a single p. Section 6 lists specific groups which
indicate both order and representation.)¶
The fundamental group operation is addition + with identity element
I. For any elements A and B of the group, A + B = B + A is
also a member of the group. Also, for any A in the group, there exists an element
-A such that A + (-A) = (-A) + A = I. Scalar multiplication by r is
equivalent to the repeated application of the group operation on an
element A with itself r-1 times, this is denoted as r*A = A + ... + A.
For any element A, p*A=I. The case when the scalar multiplication is
performed on the group generator is denoted as ScalarMultGen(r).
Given two elements A and B, the discrete logarithm problem is to find
an integer k such that B = k*A. Thus, k is the discrete logarithm of
B with respect to the base A.
The set of scalars corresponds to GF(p), a prime field of order p, and are
represented as the set of integers defined by {0, 1, ..., p-1}.
This document uses types
Element and Scalar to denote elements of the group and its set of
scalars, respectively.¶
We now detail a number of member functions that can be invoked on a prime-order group.¶
Order(): Outputs the order of the group (i.e. p).¶
Identity(): Outputs the identity element of the group (i.e. I).¶
Generator(): Outputs the fixed generator of the group.¶
HashToGroup(x, info): Deterministically maps
an array of bytes x with domain separation value info to an element of Group. The map must ensure that,
for any adversary receiving R = HashToGroup(x, info), it is
computationally difficult to reverse the mapping.
Security properties of this function are described
in [I-D.irtf-cfrg-hash-to-curve].¶
HashToScalar(x, info): Deterministically maps
an array of bytes x with domain separation value info to an element in GF(p).
Security properties of this function are described in [I-D.irtf-cfrg-hash-to-curve], Section 10.5.¶
RandomScalar(): Chooses at random a non-zero element in GF(p).¶
ScalarInverse(s): Returns the inverse of input Scalar s on GF(p).¶
SerializeElement(A): Maps an Element A
to a canonical byte array buf of fixed length Ne.¶
DeserializeElement(buf): Attempts to map a byte array buf to
an Element A, and fails if the input is not the valid canonical byte
representation of an element of the group. This function can raise a
DeserializeError if deserialization fails or A is the identity element of
the group; see Section 6 for group-specific input validation steps.¶
SerializeScalar(s): Maps a Scalar s to a canonical
byte array buf of fixed length Ns.¶
DeserializeScalar(buf): Attempts to map a byte array buf to a Scalar s.
This function can raise a DeserializeError if deserialization fails; see
Section 6 for group-specific input validation steps.¶
For each group, there exists two distinct generators, generatorG and generatorH, generatorG = G.Generator() and generatorH = G.HashToGroup(G.SerializeElement(generatorG), "generatorH"). The group member functions GeneratorG() and GeneratorH() are shorthand for returning generatorG and generatorH, respectively.¶
Section 6 contains details for the implementation of this interface for different prime-order groups instantiated over elliptic curves.¶
The ARC protocol is a two-party protocol run between client and server consisting of three distinct phases:¶
Key generation. In this phase, the server generates its private and public keys to be used for the remaining phases. This phase is described in Section 4.1.¶
Credential issuance. In this phase, the client and server interact to issue the client a credential that is cryptographically bound to client secrets. This phase is described in Section 4.2.¶
Presentation. In this phase, the client uses the credential to create a "presentation" to the server, where the server learns nothing more than whether or not the presentation is valid and corresponds to some previously issued credential, without learning which credential it corresponds to. This phase is described in Section 4.3.¶
This protocol bears resemblance to anonymous token protocols, such as those built on Blind RSA [BLIND-RSA] and Oblivious Pseudorandom Functions [OPRFS] with one critical distinction: unlike anonymous tokens, an anonymous credential can be used multiple times to create unlinkable presentations (up to the fixed presentation limit). This means that a single issuance invocation can drive multiple presentation invocations, whereas with anonymous tokens, each presentation invocation requires exactly one issuance invocation. As a result, credentials are generally longer lived than tokens. Applications configure the credential presentation limit after the credential is issued such that client and server agree on the limit during presentation. Servers are responsible for ensuring this limit is not exceeded. Clients that exceed the agreed-upon presentation limit break the unlinkability guarantees provided by the protocol.¶
The rest of this section describes the three phases of the ARC protocol.¶
In the key generation phase, the server generates its private and public keys, denoted ServerPrivateKey and ServerPublicKey, as follows.¶
Input: None Output: - ServerPrivateKey: - x0: Scalar - x1: Scalar - x2: Scalar - x0Blinding: Scalar - ServerPublicKey: - X0: Element - X1: Element - X2: Element Parameters - Group G def SetupServer(): x0 = G.RandomScalar() x1 = G.RandomScalar() x2 = G.RandomScalar() x0Blinding = G.RandomScalar() X0 = x0 * G.GeneratorG() + x0Blinding * G.GeneratorH() X1 = x1 * G.GeneratorH() X2 = x2 * G.GeneratorH() return ServerPrivateKey(x0, x1, x2, x0Blinding), ServerPublicKey(X0, X1, X2)¶
The server public key can be serialized as follows:¶
struct {
uint8 X0[Ne]; // G.SerializeElement(X0)
uint8 X1[Ne]; // G.SerializeElement(X1)
uint8 X2[Ne]; // G.SerializeElement(X2)
} ServerPublicKey;
¶
The length of this encoded response structure is NserverPublicKey = 3*Ne.¶
The purpose of the issuance phase is for the client and server to cooperatively compute a credential that is cryptographically bound to the client's secrets. Clients do not choose these secrets; they are computed by the protocol.¶
The issuance phase of the protocol requires clients to know the server public key a priori, as well as an arbitrary, application-specific request context. It requires no other input. It consists of three distinct steps:¶
The client generates and sends a credential request to the server. This credential request contains a proof that the request is valid with respect to the client's secrets and request context. See Section 4.2.1 for details about this step.¶
The server validates the credential request. If valid, it computes a credential response with the server private keys. The response includes a proof that the credential response is valid with respect to the server keys. The server sends the response to the client. See Section 4.2.2 for details about this step.¶
The client finalizes the credential by processing the server response. If valid, this step yields a credential that can then be used in the presentation phase of the protocol. See Section 4.2.3 for details about this step.¶
Each of these steps is described in the following subsections.¶
Given a request context, the process for creating a credential request is as follows:¶
(clientSecrets, request) = CreateCredentialRequest(requestContext) Inputs: - requestContext: Data, context for the credential request Outputs: - request: - m1Enc: Element, first encrypted secret. - m2Enc: Element, second encrypted secret. - requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc. - clientSecrets: - m1: Scalar, first secret. - m2: Scalar, second secret. - r1: Scalar, blinding factor for first secret. - r2: Scalar, blinding factor for second secret. Parameters: - G: Group - generatorG: Element, equivalent to G.GeneratorG() - generatorH: Element, equivalent to G.GeneratorH() def CreateCredentialRequest(requestContext): m1 = G.RandomScalar() m2 = G.HashToScalar(requestContext, "requestContext") r1 = G.RandomScalar() r2 = G.RandomScalar() m1Enc = m1 * generatorG + r1 * generatorH m2Enc = m2 * generatorG + r2 * generatorH requestProof = MakeCredentialRequestProof(m1, m2, r1, r2, m1Enc, m2Enc) request = (m1Enc, m2Enc, requestProof) clientSecrets = (m1, m2, r1, r2) return (clientSecrets, request)¶
See Section 5.2 for more details on the generation of the credential request proof.¶
The resulting request can be serialized as follows.¶
struct {
uint8 m1Enc[Ne];
uint8 m2Enc[Ne];
uint8 challenge[Ns];
uint8 response0[Ns];
uint8 response1[Ns];
uint8 response2[Ns];
uint8 response3[Ns];
} CredentialRequest;
¶
The length of this encoded request structure is Nrequest = 2*Ne + 5*Ns.¶
Given a credential request and server public and private keys, the process for creating a credential response is as follows.¶
response = CreateCredentialResponse(serverPrivateKey, serverPublicKey, request)
Inputs:
- serverPrivateKey:
- x0: Scalar (private), server private key 0.
- x1: Scalar (private), server private key 1.
- x2: Scalar (private), server private key 2.
- x0Blinding: Scalar (private), blinding value for x0.
- serverPublicKey:
- X0: Element, server public key 0.
- X1: Element, server public key 1.
- X2: Element, server public key 2.
- request:
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
- requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc.
Outputs:
- U: Element, a randomized generator for the response, `b*G`.
- encUPrime: Element, encrypted UPrime.
- X0Aux: Element, auxiliary point for X0.
- X1Aux: Element, auxiliary point for X1.
- X2Aux: Element, auxiliary point for X2.
- HAux: Element, auxiliary point for generatorH.
- responseProof: ZKProof, a proof of correct generation of
U, encUPrime, server public keys, and auxiliary points.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
Exceptions:
- VerifyError, raised when response verification fails
def CreateCredentialResponse(serverPrivateKey, serverPublicKey, request):
if VerifyCredentialRequestProof(request) == false:
raise VerifyError
b = G.RandomScalar()
U = b * generatorG
encUPrime = b * (serverPublicKey.X0 +
serverPrivateKey.x1 * request.m1Enc +
serverPrivateKey.x2 * request.m2Enc)
X0Aux = b * serverPrivateKey.x0Blinding * generatorH
X1Aux = b * serverPublicKey.X1
X2Aux = b * serverPublicKey.X2
HAux = b * generatorH
responseProof = MakeCredentialResponseProof(serverPrivateKey,
serverPublicKey, request, b, U, encUPrime, X0Aux, X1Aux, X2Aux, HAux)
return (U, encUPrime, X0Aux, X1Aux, X2Aux, HAux, responseProof)
¶
The resulting response can be serialized as follows. See Section 5.3 for more details on the generation of the credential response proof.¶
struct {
uint8 U[Ne];
uint8 encUPrime[Ne];
uint8 X0Aux[Ne];
uint8 X1Aux[Ne];
uint8 X2Aux[Ne];
uint8 HAux[Ne];
uint8 challenge[Ns];
uint8 response0[Ns];
uint8 response1[Ns];
uint8 response2[Ns];
uint8 response3[Ns];
uint8 response4[Ns];
uint8 response5[Ns];
uint8 response6[Ns];
} CredentialResponse
¶
The length of this encoded response structure is Nresponse = 6*Ne + 8*Ns.¶
Given a credential request and response, server public keys, and the client secrets produced when creating a credential request, the process for finalizing the issuance flow and creating a credential is as follows.¶
credential = FinalizeCredential(clientSecrets, serverPublicKey, request, response)
Inputs:
- clientSecrets:
- m1: Scalar, first secret.
- m2: Scalar, second secret.
- r1: Scalar, blinding factor for first secret.
- r2: Scalar, blinding factor for second secret.
- serverPublicKey: ServerPublicKey, shared with the client out-of-band
- request:
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
- requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc.
- response:
- U: Element, a randomized generator for the response. `b*G`.
- encUPrime: Element, encrypted UPrime.
- X0Aux: Element, auxiliary point for X0.
- X1Aux: Element, auxiliary point for X1.
- X2Aux: Element, auxiliary point for X2.
- HAux: Element, auxiliary point for generatorH.
- responseProof: ZKProof, a proof of correct generation of U, encUPrime, server public keys, and auxiliary points.
Outputs:
- credential:
- m1: Scalar, client's first secret.
- U: Element, a randomized generator for the response. `b*G`.
- UPrime: Element, the MAC over the server's private keys and the client's secrets.
- X1: Element, server public key 1.
Exceptions:
- VerifyError, raised when response verification fails
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def FinalizeCredential(clientSecrets, serverPublicKey, request, response):
if VerifyCredentialResponseProof(serverPublicKey, response, request) == false:
raise VerifyError
UPrime = response.encUPrime - response.X0Aux - clientSecrets.r1 * response.X1Aux - clientSecrets.r2 * response.X2Aux
return (clientSecrets.m1, response.U, UPrime, serverPublicKey.X1)
¶
The purpose of the presentation phase is for the client to create a "presentation" to the server which can be verified using the server private key. This phase is non-interactive, i.e., there is no state stored between client and server in order to produce and then verify a presentation. Client and server agree upon a fixed limit of presentations in order to create and verify presentations; presentations will not verify correctly if the client and server use different limits.¶
This phase consists of three steps:¶
The client creates a presentation state for a given presentation context and presentation limit. This state is used to produce a fixed amount of presentations.¶
The client creates a presentation from the presentation state and sends it to the server. The presentation is cryptographically bound to the state's presentation context, and contains proof that the presentation is valid with respect to the presentation context. Moreover, the presentation contains proof that the nonce (an integer) associated with this presentation is within the presentation limit. The nonce value used in each presentation is hidden in a Pedersen commitment, ensuring servers cannot link presentations using the nonce.¶
The server verifies the presentation with respect to the presentation context and presentation limit.¶
Details for each of these steps are in the following subsections.¶
Presentation state is used to track the number of presentations for a given credential. This state is important for ARC's unlinkability goals: reuse of state can break unlinkability properties of credential presentations. State is initialized with a credential, presentation context, and presentation limit. It is then mutated after each presentation construction (as described in Section 4.3.2).¶
state = MakePresentationState(credential, presentationContext, presentationLimit) Inputs: - credential: - m1: Scalar, client's first secret. - U: Element, a randomized generator for the response `b*G`. - UPrime: Element, the MAC over the server's private keys and the client's secrets. - X1: Element, server public key 1. - presentationContext: Data (public), used for presentation tag computation. - presentationLimit: Integer, the fixed presentation limit. Outputs: - credential - presentationContext: Data (public), used for presentation tag computation. - nextNonce: Integer, the next nonce that can be used. This increments by 1 for each use. - presentationLimit: Integer, the fixed presentation limit. def MakePresentationState(credential, presentationContext, presentationLimit): return PresentationState(credential, presentationContext, 0, presentationLimit)¶
Creating a presentation requires a credential, presentation context, and presentation limit. This process is necessarily stateful on the client since the number of times a credential is used for a given presentation context cannot exceed the presentation limit; doing so would break presentation unlinkability, as two presentations created with the same nonce can be directly compared for equality (via the "tag"). As a result, the process for creating a presentation accepts as input a presentation state and then outputs an updated presentation state.¶
newState, nonce, presentation = Present(state)
Inputs:
state: input PresentationState
- credential
- presentationContext: Data (public), used for presentation tag computation.
- nextNonce: Integer, the next nonce that can be used. This increments by 1 for each use.
- presentationLimit: Integer, the fixed presentation limit.
Outputs:
- newState: updated PresentationState
- nonce: Integer, the nonce associated with this presentation.
- presentation:
- U: Element, re-randomized from the U in the response.
- UPrimeCommit: Element, a public key to the issued UPrime.
- m1Commit: Element, a public key to the client secret (m1).
- tag: Element, the tag element used for enforcing the presentation limit.
- nonceCommit: Element, a Pedersen commitment to the nonce.
- presentationProof: ZKProof, a joint proof of correct generation of the presentation and that the committed nonce is in [0, presentationLimit).
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
Exceptions:
- LimitExceededError, raised when the presentation count meets or exceeds the presentation limit for the given presentation context
def Present(state):
if state.nextNonce >= state.presentationLimit:
raise LimitExceededError
nonce = state.nextNonce
# This step mutates the state by incrementing nextNonce by 1.
state.nextNonce += 1
a = G.RandomScalar()
r = G.RandomScalar()
z = G.RandomScalar()
U = a * state.credential.U
UPrime = a * state.credential.UPrime
UPrimeCommit = UPrime + r * generatorG
m1Commit = state.credential.m1 * U + z * generatorH
# Create Pedersen commitment to the nonce
nonceBlinding = G.RandomScalar()
nonceCommit = G.Scalar(nonce) * generatorG + nonceBlinding * generatorH
generatorT = G.HashToGroup(state.presentationContext, "Tag")
tag = (state.credential.m1 + nonce)^(-1) * generatorT
V = z * state.credential.X1 - r * generatorG
# Generate presentation proof with integrated range proof
presentationProof = MakePresentationProof(U, UPrimeCommit, m1Commit, tag, generatorT,
state.credential, V, r, z, nonce,
nonceBlinding, nonceCommit, state.presentationLimit)
presentation = (U, UPrimeCommit, m1Commit, tag, nonceCommit, presentationProof)
return state, nonce, presentation
¶
OPEN ISSUE: should the tag also fold in the presentation limit?¶
The resulting presentation can be serialized as follows. See Section 5.4 for more details on the generation of the presentation proof. The presentation proof integrates the range proof as described in Section 5.5.¶
struct {
uint8 U[Ne];
uint8 UPrimeCommit[Ne];
uint8 m1Commit[Ne];
uint8 tag[Ne];
uint8 nonceCommit[Ne];
PresentationProof presentationProof;
} Presentation
struct {
uint8 D[k][Ne]; // k = ceil(log2(presentationLimit))
uint8 challenge[Ns];
// Variable length based on presentation variables plus range proof variables
uint8 responses[5 + 3 * k][Ns];
} PresentationProof
¶
The length of the Presentation structure is Npresentation = 5*Ne + Npresentationproof.
Npresentationproof = k * Ne + (6 + 3 * k) * Ns, which includes the D commitments (k * Ne), the challenge (Ns), the response scalars for presentation variables (5 scalars: m1, z, -r, nonce, nonceBlinding), and range proof variables (3 * k scalars: b[i], s[i], s2[i] for each bit).
k is the number of bits it takes to represent the presentationLimit, i.e., k = ceil(log2(presentationLimit))¶
The server processes the presentation by verifying the integrated presentation proof, which includes verification of the range proof, against server-computed values. Note that the server does not receive the raw nonce value, only the Pedersen commitment to it.¶
validity, tag = VerifyPresentation(
serverPrivateKey,
serverPublicKey,
requestContext,
presentationContext,
presentation,
presentationLimit)
Inputs:
- serverPrivateKey:
- x0: Scalar (private), server private key 0.
- x1: Scalar (private), server private key 1.
- x2: Scalar (private), server private key 2.
- x0Blinding: Scalar (private), blinding value for x0.
- serverPublicKey:
- X0: Element, server public key 0.
- X1: Element, server public key 1.
- X2: Element, server public key 2.
- requestContext: Data, context for the credential request.
- presentationContext: Data (public), used for presentation tag computation.
- presentation:
- U: Element, re-randomized from the U in the response.
- UPrimeCommit: Element, a public key to the issued UPrime.
- m1Commit: Element, a public key to the client secret (m1).
- tag: Element, the tag element used for enforcing the presentation limit.
- nonceCommit: Element, a Pedersen commitment to the nonce.
- presentationProof: ZKProof, a joint proof of correct generation of the presentation and that the committed nonce is in [0, presentationLimit).
- presentationLimit: Integer, the fixed presentation limit.
Outputs:
- validity: Boolean, True if the presentation is valid, False otherwise.
- tag: Bytes, the value of the presentation tag used for rate limiting.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def VerifyPresentation(
serverPrivateKey,
serverPublicKey,
requestContext,
presentationContext,
presentation,
presentationLimit):
# The presentation proof verifies the relationship between the tag,
# m1, and the committed nonce using zero-knowledge techniques, without
# learning the value of the nonce. It also includes verification that
# the committed nonce is in [0, presentationLimit).
validity = VerifyPresentationProof(
serverPrivateKey,
serverPublicKey,
requestContext,
presentationContext,
presentation,
presentationLimit)
return validity, presentation.tag
¶
Implementation-specific steps: the server must perform a check that the tag (presentation.tag) has not previously been seen, to prevent double spending. It then stores the tag for use in future double spending checks. To reduce the overhead of performing double spend checks, the server can store and look up the tags corresponding to the associated requestContext and presentationContext values.¶
This section describes a Schnorr proof compiler that is used for the construction of other proofs needed throughout the ARC protocol. Section 5.1 describes the compiler, and the remaining sections describe how it is used for the purposes of producing ARC proofs.¶
The compiler specified in this section automates the Fiat-Shamir transform that is often used to transform interactive zero-knowledge proofs into non-interactive proofs such that they can be used to non-interactively prove various statements of importance in higher-level protocols, such as ARC. The compiler consists of a prover and verifier role. The prover constructs a transcript for the proof and then applies the Fiat-Shamir heuristic to generate the resulting challenge and response values. The verifier reconstructs the same transcript to verify the proof.¶
The prover and verifier roles are specified below in Section 5.1.1 and Section 5.1.2, respectively.¶
The prover role consists of four functions:¶
AppendScalar: This function adds a scalar representation to the transcript.¶
AppendElement: This function adds an element representation to the transcript.¶
Constrain: This function applies an explicit constraint to the proof, where the constraint is expressed as equality between some element and a linear combination of scalar and element representations. An example constraint might be Z = aX + bY, for scalars a, b, and elements
X, Y, Z.¶
Prove: This function applies the Fiat-Shamir heuristic to the protocol transcript and set of constraints to produce a zero-knowledge proof that can be verified.¶
These functions are defined in the following sub-sections.¶
In addition, the prover role consists of the following state:¶
label: Data, a value representing the context in which the proof will be used¶
scalars: [Integer], An ordered set of representation of scalar variables to use in the proof. Each scalar has a label associated with it, stored in a list called scalar_labels.¶
elements: [Integer], An ordered set of representation of element variables to use in the proof. Each element has a label associated with it, stored in a list called element_labels.¶
constraints: a set of constraints, where each constraint consists of a constraint element and a linear combination of variables.¶
AppendScalar(label, assignment) Inputs: - label: Data, Scalar variable label - assignment: Scalar variable Outputs: - Integer representation of the new scalar variable def AppendScalar(label, assignment): state.scalars.append(assignment) state.scalar_labels.append(label) return len(state.scalars) - 1¶
AppendElement(label, assignment) Inputs: - label: Data, Element variable label - assignment: Element variable Outputs: - Integer representation of the new element variable def AppendElement(label, assignment): state.elements.append(assignment) state.element_labels.append(label) return len(state.elements) - 1¶
Constrain(constraintElement, linearCombination) Inputs: - constraintElement: Integer, representation of constraint element - linearCombination: linear combination of scalar and element variable (representations) def Constrain(constraintElement, linearCombination): state.constraints.append((constraintElement, linearCombination))¶
The Prove function is defined below.¶
Prove()
Outputs:
- ZKProof, a proof consisting of a challenge Scalar and then fixed number of response Scalar values
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
Exceptions:
- InvalidVariableAllocationError, raised when the prover was incorrectly configured
def Prove():
blindings = [G.RandomScalar() for i in range(len(state.scalars))]
blinded_elements = []
for (constraint_point, linear_combination) in state.constraints:
if constraint_point.index > len(state.elements):
raise InvalidVariableAllocationError
for (scalar_var, element_var) in linear_combination:
if scalar_var.index > len(state.scalars):
raise InvalidVariableAllocationError
if element_var.index > len(state.elements):
raise InvalidVariableAllocationError
scalar_index = linear_combination[0][0].index
element_index = linear_combination[0][1].index
blinded_element = blindings[scalar_index] * state.elements[element_index]
for i, pair in enumerate(linear_combination):
if i > 0:
scalar_index = pair[0].index
element_index = pair[1].index
blinded_element += blindings[scalar_index] * state.elements[element_index]
blinded_elements.append(blinded_element)
# Obtain a scalar challenge
challenge = ComposeChallenge(state.label, state.elements, blinded_elements)
# Compute response scalars from the challenge, scalars, and blindings.
responses = []
for (index, scalar) in enumerate(state.scalars):
blinding = blindings[index]
responses.append(blinding - challenge * scalar)
return ZKProof(challenge, responses)
¶
The function ComposeChallenge is defined below.¶
ComposeChallenge(label, elements, blinded_elements)
Inputs:
- label: Data, the proof label
- elements: [Element], ordered list of elements
- blinded_elements: [Element], ordered list of blinded elements
Outputs:
- challenge, Scalar
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def ComposeChallenge(label, elements, blinded_elements):
challenge_input = Data() # Empty Data
for element in elements:
serialized_element = G.SerializeElement(element)
challenge_input += I2OSP(len(serialized_element), 2) + serialized_element
for blinded_element in blinded_elements:
serialized_blinded_element = G.SerializeElement(blinded_element)
challenge_input += I2OSP(len(serialized_blinded_element), 2) + serialized_blinded_element
return G.HashToScalar(challenge_input, label)
¶
The verifier role consists of four functions:¶
AppendScalar: This function adds a scalar representation to the transcript.¶
AppendElement: This function adds an element representation to the transcript.¶
Constrain: This function applies an explicit constraint to the proof, where the constraint is expressed as equality between some element and a linear combination of scalar and element representations. An example constraint might be Z = aX + bY, for scalars a, b, and elements
X, Y, Z.¶
Verify: This function applies the Fiat-Shamir heuristic to verify the zero-knowledge proof.¶
AppendScalar and Verify are defined in the following sub-sections. AppendElement and Constrain matches the functionality used in the prover role.¶
AppendScalar(label) Inputs: - label: Data, Scalar variable label Outputs: - Integer representation of the new scalar variable def AppendScalar(label): state.scalar_labels.append(label) return len(state.scalar_labels) - 1¶
Verify(proof)
Inputs:
- ZKProof, a proof consisting of a challenge Scalar and then fixed number of response Scalar values
Outputs:
- Boolean, True if the proof is valid, False otherwise.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
Exceptions:
- InvalidVariableAllocationError, raised when the prover was incorrectly configured
def Verify(proof):
if len(state.elements) != len(state.element_labels):
raise InvalidVariableAllocationError
blinded_elements = []
for (constraint_element, linear_combination) in state.constraints:
if constraint_element.index > len(state.elements):
raise InvalidVariableAllocationError
for (_, element_var) in linear_combination:
if element_var.index > len(state.elements):
raise InvalidVariableAllocationError
challenge_element = proof.challenge * state.elements[constraint_element.index]
for i, pair in enumerate(linear_combination):
challenge_element += proof.responses[pair[0].index] * state.elements[pair[1].index]
blinded_elements.append(challenge_element)
challenge = ComposeChallenge(state.label, state.elements, blinded_elements)
return challenge == proof.challenge
¶
The request proof is a proof of knowledge of (m1, m2, r1, r2) used to generate the encrypted request. Statements to prove:¶
1. m1Enc = m1 * generatorG + r1 * generatorH 2. m2Enc = m2 * generatorG + r2 * generatorH¶
requestProof = MakeCredentialRequestProof(m1, m2, r1, r2, m1Enc, m2Enc)
Inputs:
- m1: Scalar, first secret.
- m2: Scalar, second secret.
- r1: Scalar, blinding factor for first secret.
- r2: Scalar, blinding factor for second secret.
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
Outputs:
- proof: ZKProof
- challenge: Scalar, the challenge used in the proof of valid encryption.
- response0: Scalar, the response corresponding to m1.
- response1: Scalar, the response corresponding to m2.
- response2: Scalar, the response corresponding to r1.
- response3: Scalar, the response corresponding to r2.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
- contextString: public input
def MakeCredentialRequestProof(m1, m2, r1, r2, m1Enc, m2Enc):
prover = Prover(contextString + "CredentialRequest")
m1Var = prover.AppendScalar("m1", m1)
m2Var = prover.AppendScalar("m2", m2)
r1Var = prover.AppendScalar("r1", r1)
r2Var = prover.AppendScalar("r2", r2)
genGVar = prover.AppendElement("genG", generatorG)
genHVar = prover.AppendElement("genH", generatorH)
m1EncVar = prover.AppendElement("m1Enc", m1Enc)
m2EncVar = prover.AppendElement("m2Enc", m2Enc)
# 1. m1Enc = m1 * generatorG + r1 * generatorH
prover.Constrain(m1EncVar, [(m1Var, genGVar), (r1Var, genHVar)])
# 2. m2Enc = m2 * generatorG + r2 * generatorH
prover.Constrain(m2EncVar, [(m2Var, genGVar), (r2Var, genHVar)])
return prover.Prove()
¶
validity = VerifyCredentialRequestProof(request)
Inputs:
- request:
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
- requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc.
- challenge: Scalar, the challenge used in the proof of valid encryption.
- response0: Scalar, the response corresponding to m1.
- response1: Scalar, the response corresponding to m2.
- response2: Scalar, the response corresponding to r1.
- response3: Scalar, the response corresponding to r2.
Outputs:
- validity: Boolean, True if the proof verifies correctly, False otherwise.
Parameters:
- G: group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
- contextString: public input
def VerifyCredentialRequestProof(request):
verifier = Verifier(contextString + "CredentialRequest")
m1Var = verifier.AppendScalar("m1")
m2Var = verifier.AppendScalar("m2")
r1Var = verifier.AppendScalar("r1")
r2Var = verifier.AppendScalar("r2")
genGVar = verifier.AppendElement("genG", generatorG)
genHVar = verifier.AppendElement("genH", generatorH)
m1EncVar = verifier.AppendElement("m1Enc", request.m1Enc)
m2EncVar = verifier.AppendElement("m2Enc", request.m2Enc)
# 1. m1Enc = m1 * generatorG + r1 * generatorH
verifier.Constrain(m1EncVar, [(m1Var, genGVar), (r1Var, genHVar)])
# 2. m2Enc = m2 * generatorG + r2 * generatorH
verifier.Constrain(m2EncVar, [(m2Var, genGVar), (r2Var, genHVar)])
return verifier.Verify(request.requestProof)
¶
The response proof is a proof of knowledge of (x0, x1, x2, x0Blinding, b) used in the server's CredentialResponse for the client's CredentialRequest. Statements to prove:¶
1. X0 = x0 * generatorG + x0Blinding * generatorH 2. X1 = x1 * generatorH 3. X2 = x2 * generatorH 4. X0Aux = b * x0Blinding * generatorH 4a. HAux = b * generatorH 4b: X0Aux = x0Blinding * HAux (= b * x0Blinding * generatorH) 5. X1Aux = b * x1 * generatorH 5a. X1Aux = t1 * generatorH (t1 = b * x1) 5b. X1Aux = b * X1 (X1 = x1 * generatorH) 6. X2Aux = b * x2 * generatorH 6a. X2Aux = b * X2 (X2 = x2 * generatorH) 6b. X2Aux = t2 * generatorH (t2 = b * x2) 7. U = b * generatorG 8. encUPrime = b * (X0 + x1 * Enc(m1) + x2 * Enc(m2))¶
responseProof = MakeCredentialResponseProof(serverPrivateKey, serverPublicKey, request, b, U, encUPrime, X0Aux, X1Aux, X2Aux, HAux)
Inputs:
- serverPrivateKey:
- x0: Scalar (private), server private key 0.
- x1: Scalar (private), server private key 1.
- x2: Scalar (private), server private key 2.
- x0Blinding: Scalar (private), blinding value for x0.
- serverPublicKey:
- X0: Element, server public key 0.
- X1: Element, server public key 1.
- X2: Element, server public key 2.
- request:
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
- requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc.
- encUPrime: Element, encrypted UPrime.
- X0Aux: Element, auxiliary point for X0.
- X1Aux: Element, auxiliary point for X1.
- X2Aux: Element, auxiliary point for X2.
- HAux: Element, auxiliary point for generatorH.
Outputs:
- proof: ZKProof
- challenge: Scalar, the challenge used in the proof of valid response.
- response0: Scalar, the response corresponding to x0.
- response1: Scalar, the response corresponding to x1.
- response2: Scalar, the response corresponding to x2.
- response3: Scalar, the response corresponding to x0Blinding.
- response4: Scalar, the response corresponding to b.
- response5: Scalar, the response corresponding to t1.
- response6: Scalar, the response corresponding to t2.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
- contextString: public input
def MakeCredentialResponseProof(serverPrivateKey, serverPublicKey, request, b, U, encUPrime, X0Aux, X1Aux, X2Aux, HAux):
prover = Prover(contextString + "CredentialResponse")
x0Var = prover.AppendScalar("x0", serverPrivateKey.x0)
x1Var = prover.AppendScalar("x1", serverPrivateKey.x1)
x2Var = prover.AppendScalar("x2", serverPrivateKey.x2)
x0BlindingVar = prover.AppendScalar("x0Blinding", serverPrivateKey.x0Blinding)
bVar = prover.AppendScalar("b", b)
t1Var = prover.AppendScalar("t1", b * serverPrivateKey.x1)
t2Var = prover.AppendScalar("t2", b * serverPrivateKey.x2)
genGVar = prover.AppendElement("genG", generatorG)
genHVar = prover.AppendElement("genH", generatorH)
m1EncVar = prover.AppendElement("m1Enc", request.m1Enc)
m2EncVar = prover.AppendElement("m2Enc", request.m2Enc)
UVar = prover.AppendElement("U", U)
encUPrimeVar = prover.AppendElement("encUPrime", encUPrime)
X0Var = prover.AppendElement("X0", serverPublicKey.X0)
X1Var = prover.AppendElement("X1", serverPublicKey.X1)
X2Var = prover.AppendElement("X2", serverPublicKey.X2)
X0AuxVar = prover.AppendElement("X0Aux", X0Aux)
X1AuxVar = prover.AppendElement("X1Aux", X1Aux)
X2AuxVar = prover.AppendElement("X2Aux", X2Aux)
HAuxVar = prover.AppendElement("HAux", HAux)
# 1. X0 = x0 * generatorG + x0Blinding * generatorH
prover.Constrain(X0Var, [(x0Var, genGVar), (x0BlindingVar, genHVar)])
# 2. X1 = x1 * generatorH
prover.Constrain(X1Var, [(x1Var, genHVar)])
# 3. X2 = x2 * generatorH
prover.Constrain(X2Var, [(x2Var, genHVar)])
# 4. X0Aux = b * x0Blinding * generatorH
# 4a. HAux = b * generatorH
prover.Constrain(HAuxVar, [(bVar, genHVar)])
# 4b: X0Aux = x0Blinding * HAux (= b * x0Blinding * generatorH)
prover.Constrain(X0AuxVar, [(x0BlindingVar, HAuxVar)])
# 5. X1Aux = b * x1 * generatorH
# 5a. X1Aux = t1 * generatorH (t1 = b * x1)
prover.Constrain(X1AuxVar, [(t1Var, genHVar)])
# 5b. X1Aux = b * X1 (X1 = x1 * generatorH)
prover.Constrain(X1AuxVar, [(bVar, X1Var)])
# 6. X2Aux = b * x2 * generatorH
# 6a. X2Aux = b * X2 (X2 = x2 * generatorH)
prover.Constrain(X2AuxVar, [(bVar, X2Var)])
# 6b. X2Aux = t2 * H (t2 = b * x2)
prover.Constrain(X2AuxVar, [(t2Var, genHVar)])
# 7. U = b * generatorG
prover.Constrain(UVar, [(bVar, genGVar)])
# 8. encUPrime = b * (X0 + x1 * Enc(m1) + x2 * Enc(m2))
# simplified: encUPrime = b * X0 + t1 * m1Enc + t2 * m2Enc, since t1 = b * x1 and t2 = b * x2
prover.Constrain(encUPrimeVar, [(bVar, X0Var), (t1Var, m1EncVar), (t2Var, m2EncVar)])
return prover.Prove()
¶
validity = VerifyCredentialResponseProof(serverPublicKey, response, request)
Inputs:
- serverPublicKey:
- X0: Element, server public key 0.
- X1: Element, server public key 1.
- X2: Element, server public key 2.
- response:
- U: Element, a randomized generator for the response. `b*G`.
- encUPrime: Element, encrypted UPrime.
- X0Aux: Element, auxiliary point for X0.
- X1Aux: Element, auxiliary point for X1.
- X2Aux: Element, auxiliary point for X2.
- HAux: Element, auxiliary point for generatorH.
- responseProof: ZKProof, a proof of correct generation of U, encUPrime, server public keys, and auxiliary points.
- challenge: Scalar, the challenge used in the proof of valid response.
- response0: Scalar, the response corresponding to x0.
- response1: Scalar, the response corresponding to x1.
- response2: Scalar, the response corresponding to x2.
- response3: Scalar, the response corresponding to x0Blinding.
- response4: Scalar, the response corresponding to b.
- response5: Scalar, the response corresponding to t1.
- response6: Scalar, the response corresponding to t2.
- request:
- m1Enc: Element, first encrypted secret.
- m2Enc: Element, second encrypted secret.
- requestProof: ZKProof, a proof of correct generation of m1Enc and m2Enc.
Outputs:
- validity: Boolean, True if the proof verifies correctly, False otherwise.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def VerifyCredentialResponseProof(serverPublicKey, response, request):
verifier = Verifier(contextString + "CredentialResponse")
x0Var = verifier.AppendScalar("x0")
x1Var = verifier.AppendScalar("x1")
x2Var = verifier.AppendScalar("x2")
x0BlindingVar = verifier.AppendScalar("x0Blinding")
bVar = verifier.AppendScalar("b")
t1Var = verifier.AppendScalar("t1")
t2Var = verifier.AppendScalar("t2")
genGVar = verifier.AppendElement("genG", generatorG)
genHVar = verifier.AppendElement("genH", generatorH)
m1EncVar = verifier.AppendElement("m1Enc", request.m1Enc)
m2EncVar = verifier.AppendElement("m2Enc", request.m2Enc)
UVar = verifier.AppendElement("U", response.U)
encUPrimeVar = verifier.AppendElement("encUPrime", response.encUPrime)
X0Var = verifier.AppendElement("X0", serverPublicKey.X0)
X1Var = verifier.AppendElement("X1", serverPublicKey.X1)
X2Var = verifier.AppendElement("X2", serverPublicKey.X2)
X0AuxVar = verifier.AppendElement("X0Aux", response.X0Aux)
X1AuxVar = verifier.AppendElement("X1Aux", response.X1Aux)
X2AuxVar = verifier.AppendElement("X2Aux", response.X2Aux)
HAuxVar = verifier.AppendElement("HAux", response.HAux)
# 1. X0 = x0 * generatorG + x0Blinding * generatorH
verifier.Constrain(X0Var, [(x0Var, genGVar), (x0BlindingVar, genHVar)])
# 2. X1 = x1 * generatorH
verifier.Constrain(X1Var, [(x1Var, genHVar)])
# 3. X2 = x2 * generatorH
verifier.Constrain(X2Var, [(x2Var, genHVar)])
# 4. X0Aux = b * x0Blinding * generatorH
# 4a. HAux = b * generatorH
verifier.Constrain(HAuxVar, [(bVar, genHVar)])
# 4b: X0Aux = x0Blinding * HAux (= b * x0Blinding * generatorH)
verifier.Constrain(X0AuxVar, [(x0BlindingVar, HAuxVar)])
# 5. X1Aux = b * x1 * generatorH
# 5a. X1Aux = t1 * generatorH (t1 = b * x1)
verifier.Constrain(X1AuxVar, [(t1Var, genHVar)])
# 5b. X1Aux = b * X1 (X1 = x1 * generatorH)
verifier.Constrain(X1AuxVar, [(bVar, X1Var)])
# 6. X2Aux = b * x2 * generatorH
# 6a. X2Aux = b * X2 (X2 = x2 * generatorH)
verifier.Constrain(X2AuxVar, [(bVar, X2Var)])
# 6b. X2Aux = t2 * H (t2 = b * x2)
verifier.Constrain(X2AuxVar, [(t2Var, genHVar)])
# 7. U = b * generatorG
verifier.Constrain(UVar, [(bVar, genGVar)])
# 8. encUPrime = b * (X0 + x1 * Enc(m1) + x2 * Enc(m2))
# simplified: encUPrime = b * X0 + t1 * m1Enc + t2 * m2Enc, since t1 = b * x1 and t2 = b * x2
verifier.Constrain(encUPrimeVar, [(bVar, X0Var), (t1Var, m1EncVar), (t2Var, m2EncVar)])
return verifier.Verify(response.responseProof)
¶
The presentation proof is a proof of knowledge of (m1, r, z, nonce, nonceBlinding) used in the presentation, as well as a proof that nonce is in the range [0, presentationLimit).¶
Statements to prove:¶
# The m1 commitment was correctly formed
1. m1Commit = m1 * U + z * generatorH
# Other presentation elements are consistent with the credential
2. V = z * X1 - r * generatorG
# The nonceCommit is a Pedersen commitment to nonce with blinding factor nonceBlinding
3. nonceCommit = nonce * generatorG + nonceBlinding * generatorH
# The tag was correctly computed using m1 and the nonce
4. T = m1 * tag + nonce * tag, where T = G.HashToGroup(presentationContext, "Tag")
# The nonce is in the range [0, presentationLimit)
5. constraints added by the range proof. See {#range-proof}.
¶
presentationProof = MakePresentationProof(U, UPrimeCommit, m1Commit, tag, generatorT, credential, V, r, z, nonce, nonceBlinding, nonceCommit, presentationLimit)
Inputs:
- U: Element, re-randomized from the U in the response.
- UPrimeCommit: Element, a public key to the MACGGM output UPrime.
- m1Commit: Element, a public key to the client secret (m1).
- tag: Element, the tag element used for enforcing the presentation limit.
- generatorT: Element, used for presentation tag computation.
- credential:
- m1: Scalar, client's first secret.
- U: Element, a randomized generator for the response. `b*G`.
- UPrime: Element, the MAC over the server's private keys and the client's secrets.
- X1: Element, server public key 1.
- V: Element, a proof helper element.
- r: Scalar (private), a randomly generated element used in presentation.
- z: Scalar (private), a randomly generated element used in presentation.
- nonce: Int, the nonce associated with the presentation.
- nonceBlinding: Scalar (private), the blinding factor for the nonce commitment.
- nonceCommit: Element, the Pedersen commitment to the nonce.
- presentationLimit: Integer, the fixed presentation limit.
Outputs:
- presentationProof: ZKProof, a joint proof covering both presentation and range proof
- D: [Element], array of commitments to the bit decomposition of the nonce
- challenge: Scalar, the challenge used in the proof of valid presentation.
- response: [Scalar], an array of scalars for all variables (presentation + range proof)
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
- contextString: public input
def MakePresentationProof(U, UPrimeCommit, m1Commit, tag, generatorT, credential, V, r, z, nonce, nonceBlinding, nonceCommit, presentationLimit):
prover = Prover(contextString + "CredentialPresentation")
m1Var = prover.AppendScalar("m1", credential.m1)
zVar = prover.AppendScalar("z", z)
rNegVar = prover.AppendScalar("-r", -r)
nonceVar = prover.AppendScalar("nonce", nonce)
nonceBlindingVar = prover.AppendScalar("nonceBlinding", nonceBlinding)
genGVar = prover.AppendElement("genG", generatorG)
genHVar = prover.AppendElement("genH", generatorH)
UVar = prover.AppendElement("U", U)
_ = prover.AppendElement("UPrimeCommit", UPrimeCommit)
m1CommitVar = prover.AppendElement("m1Commit", m1Commit)
VVar = prover.AppendElement("V", V)
X1Var = prover.AppendElement("X1", credential.X1)
tagVar = prover.AppendElement("tag", tag)
genTVar = prover.AppendElement("genT", generatorT)
nonceCommitVar = prover.AppendElement("nonceCommit", nonceCommit)
# 1. m1Commit = m1 * U + z * generatorH
prover.Constrain(m1CommitVar, [(m1Var, UVar), (zVar, genHVar)])
# 2. V = z * X1 - r * generatorG
prover.Constrain(VVar, [(zVar, X1Var), (rNegVar, genGVar)])
# 3. nonceCommit = nonce * generatorG + nonceBlinding * generatorH
prover.Constrain(nonceCommitVar, [(nonceVar, genGVar), (nonceBlindingVar, genHVar)])
# 4. T = m1 * tag + nonce * tag, where T = G.HashToGroup(presentationContext, "Tag")
prover.Constrain(genTVar, [(m1Var, tagVar), (nonceVar, tagVar)])
# 5. Add range proof constraints
(prover, D) = MakeRangeProofHelper(prover, nonce, nonceBlinding, presentationLimit,
genGVar, genHVar)
# Generate the joint proof
presentationProof = prover.Prove()
return (presentationProof, D)
¶
validity = VerifyPresentationProof(
serverPrivateKey,
serverPublicKey,
requestContext,
presentationContext,
presentation,
presentationLimit)
Inputs:
- serverPrivateKey:
- x0: Scalar (private), server private key 0.
- x1: Scalar (private), server private key 1.
- x2: Scalar (private), server private key 2.
- x0Blinding: Scalar (private), blinding value for x0.
- serverPublicKey:
- X0: Element, server public key 0.
- X1: Element, server public key 1.
- X2: Element, server public key 2.
- requestContext: Data, context for the credential request.
- presentationContext: Data (public), used for presentation tag computation.
- presentation:
- U: Element, re-randomized from the U in the response.
- UPrimeCommit: Element, a public key to the issued UPrime.
- m1Commit: Element, a public key to the client secret (m1).
- tag: Element, the tag element used for enforcing the presentation limit.
- nonceCommit: Element, a Pedersen commitment to the nonce.
- D: [Element], array of commitments to the bit decomposition of nonceCommit
- presentationProof: ZKProof, a joint proof covering both presentation and range proof
- challenge: Scalar, the challenge used in the proof of valid presentation.
- response: [Scalar], an array of scalars for all variables (presentation + range proof)
- presentationLimit: Integer, the fixed presentation limit.
Outputs:
- validity: Boolean, True if the proof verifies correctly, False otherwise.
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
- contextString: public input
def VerifyPresentationProof(
serverPrivateKey,
serverPublicKey,
requestContext,
presentationContext,
presentation,
presentationLimit):
m2 = G.HashToScalar(requestContext, "requestContext")
V = serverPrivateKey.x0 * presentation.U + serverPrivateKey.x1 * presentation.m1Commit + serverPrivateKey.x2 * m2 * presentation.U - presentation.UPrimeCommit
generatorT = G.HashToGroup(presentationContext, "Tag")
verifier = Verifier(contextString + "CredentialPresentation")
m1Var = verifier.AppendScalar("m1")
zVar = verifier.AppendScalar("z")
rNegVar = verifier.AppendScalar("-r")
nonceVar = verifier.AppendScalar("nonce")
nonceBlindingVar = verifier.AppendScalar("nonceBlinding")
genGVar = verifier.AppendElement("genG", generatorG)
genHVar = verifier.AppendElement("genH", generatorH)
UVar = verifier.AppendElement("U", presentation.U)
_ = verifier.AppendElement("UPrimeCommit", presentation.UPrimeCommit)
m1CommitVar = verifier.AppendElement("m1Commit", presentation.m1Commit)
VVar = verifier.AppendElement("V", V)
X1Var = verifier.AppendElement("X1", serverPublicKey.X1)
tagVar = verifier.AppendElement("tag", presentation.tag)
genTVar = verifier.AppendElement("genT", generatorT)
nonceCommitVar = verifier.AppendElement("nonceCommit", presentation.nonceCommit)
# 1. m1Commit = m1 * U + z * generatorH
verifier.Constrain(m1CommitVar, [(m1Var, UVar), (zVar, genHVar)])
# 2. V = z * X1 - r * generatorG
verifier.Constrain(VVar, [(zVar, X1Var), (rNegVar, genGVar)])
# 3. nonceCommit = nonce * generatorG + nonceBlinding * generatorH
verifier.Constrain(nonceCommitVar, [(nonceVar, genGVar), (nonceBlindingVar, genHVar)])
# 4. G.HashToGroup(presentationContext, "Tag") = m1 * tag + nonce * tag
verifier.Constrain(genTVar, [(m1Var, tagVar), (nonceVar, tagVar)])
# 5. Add range proof constraints and verify the sum of the nonceCommit bit commitments
(verifier, sumValid) = VerifyRangeProofHelper(verifier, presentation.D, presentation.nonceCommit,
presentationLimit, genGVar, genHVar)
# Verify the joint proof
proofValid = sumValid and verifier.Verify(presentation.presentationProof)
return proofValid
¶
This section specifies a range proof to prove a secret value nonce lies
in an arbitrary interval [0, presentationLimit). Before specifying the proof system, we first
give a brief overview of how it works. For simplicity, assume that presentationLimit is a
power of two, that is, presentationLimit = 2^k for some integer k > 0.¶
To prove a value lies in [0,(2^k)-1), we prove it has a valid k-bit representation.
This is proven by committing to the full value nonce, then all bits of the bit decomposition
b of the value nonce, and then proving each coefficient of the bit decomposition is
actually 0 or 1 and that the sum of the bits multiplied by their associated bases equals
the full value nonce.
This involves the following steps:¶
Commit to the bits of nonce. That is, for each bit b[i] of the k-bit decomposition of nonce,
let D[i] = b[i] * generatorG + s[i] * generatorH, where s[i] is a blinding scalar.¶
Prove that b[i] is in {0,1} by proving the algebraic relation b[i] *
(b[i]-1) == 0 holds. This quadratic relation can be linearized by
adding an auxilary witness s2[i] and adding the linear relation
D[i] = b[i] * D[i] + s2[i] * generatorH to the equation system.
A valid witness s2[i] can only be computed by the prover if b[i] is in {0,1},
and is computed as s2[i] = (1 - b[i]) * s[i]. Successfully computing a witness for
any other value, while satisfying the linear relation constraints, requires the prover
to break the discrete logarithm problem.¶
In addition to verifying the proof of the above relation, the verifier checks that the sum of the bit
commitments is equal to the sum of the commitment to nonce:¶
nonceCommit = D[0] * 2^0 + D[1] * 2^1 + D[2] * 2^2 + ... + D[k-1] * 2^{k-1}
¶
The third step is verified outside of the proof by adding the commitments homomorphically.¶
To support the general case, where presentationLimit is not necessarily a power of two,
we extend the range proof for arbitrary ranges by decomposing the range
up to the second highest power of two and adding an additional, non-binary range that
covers the remaining range. This is detailed in ComputeBases below.¶
bases = ComputeBases(presentationLimit)
Inputs:
- presentationLimit: Integer, the maximum value of the range (exclusive).
Outputs:
- bases: an array of Scalar bases to represent elements, sorted in descending order. A base is
either a power of two or a unique remainder that can be used to represent any integer
in [0, presentationLimit).
def ComputeBases(presentationLimit):
# compute bases to express the commitment as a linear combination of the bit decomposition
remainder = presentationLimit
bases = []
k = ceil(log2(presentationLimit))
# Generate all but the last power-of-two base.
for i in range(k - 1):
base = 2 ** i
remainder -= base
bases.append(base)
bases.append(remainder - 1) # add non-binary base to close the gap
# call sorted on array to ensure the additional base is in correct order
return sorted(bases, reverse=True)
¶
Note that by extending the range proof for arbitrary ranges, we are changing the bases used for decomposition and therefore introducing the potential for multiple valid decompositions of a value (the nonce). Implementations compliant with this specification MUST follow the canonical decomposition defined in Section 5.5.1.¶
Using the bases from ComputeBases, the function MakeRangeProofHelper
represents the secret nonce as a linear combination of the bases, using the resulting
bit representation to generate the cryptographic commitments and witness values for the
range proof. This helper function is called from within MakePresentationProof to add
range proof constraints to the presentation proof statement.¶
(prover, D) = MakeRangeProofHelper(prover, nonce, nonceBlinding, presentationLimit,
genGVar, genHVar)
Inputs:
- prover: Prover statement to which constraints will be added
- nonce: Integer, the nonce value to prove is in range
- nonceBlinding: Scalar, the blinding factor for the nonce commitment
- presentationLimit: Integer, the maximum value of the range (exclusive).
- genGVar: Integer, variable index for generator G
- genHVar: Integer, variable index for generator H
Outputs:
- prover: Modified prover statement with range proof constraints added
- D: [Element], array of commitments to the bit decomposition of nonceCommit
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def MakeRangeProofHelper(prover, nonce, nonceBlinding, presentationLimit,
genGVar, genHVar):
# Compute bit decomposition and commitments
bases = ComputeBases(presentationLimit)
# Compute bit decomposition of nonce
b = []
remainder = nonce
# must run in constant-time (branching depends on secret value)
for base in bases:
bitValue = 1 if (remainder >= base) else 0
remainder -= bitValue * base
b.append(G.Scalar(bitValue))
# Compute commitments to bits
D = []
s = []
s2 = []
partial_sum = G.Scalar(0)
for i in range(len(bases) - 1):
s.append(G.RandomScalar())
partial_sum += bases[i] * s[i]
s2.append((G.Scalar(1) - b[i]) * s[i])
D.append(b[i] * generatorG + s[i] * generatorH)
# Blinding value for the last bit commitment is chosen strategically
# so that all the bit commitments will sum up to nonceCommit.
idx = len(bases) - 1
s.append(G.ScalarInverse(bases[idx]) * (nonceBlinding - partial_sum))
s2.append((G.Scalar(1) - b[idx]) * s[idx])
D.append(b[idx] * generatorG + s[idx] * generatorH)
vars_b = []
vars_s = []
vars_s2 = []
vars_D = []
for i in range(len(b)):
# Append scalar variables with witness values
vars_b.append(prover.AppendScalar("b" + str(i), b[i]))
vars_s.append(prover.AppendScalar("s" + str(i), s[i]))
vars_s2.append(prover.AppendScalar("s2" + str(i), s2[i]))
# Append element variables for bit commitments D
vars_D.append(prover.AppendElement("D" + str(i), D[i]))
# Add constraints proving each b[i] is in {0,1}
for i in range(len(b)):
# D[i] = b[i] * generatorG + s[i] * generatorH
prover.Constrain(vars_D[i], [(vars_b[i], genGVar), (vars_s[i], genHVar)])
# D[i] = b[i] * D[i] + s2[i] * generatorH (proves b[i] is in {0,1})
prover.Constrain(vars_D[i], [(vars_b[i], vars_D[i]), (vars_s2[i], genHVar)])
return (prover, D)
¶
(verifier, sumValid) = VerifyRangeProofHelper(verifier, D, nonceCommit, presentationLimit,
genGVar, genHVar)
Inputs:
- verifier: Verifier statement to which constraints will be added
- D: [Element], array of commitments to the bit decomposition of nonceCommit
- nonceCommit: Element, the Pedersen commitment to the nonce
- presentationLimit: Integer, the maximum value of the range (exclusive).
- genGVar: Integer, variable index for generator G
- genHVar: Integer, variable index for generator H
Outputs:
- verifier: Modified verifier statement with range proof constraints added
- validity: Boolean, True if sum(bases[i] * D[i]) == nonceCommit, False otherwise
Parameters:
- G: Group
- generatorG: Element, equivalent to G.GeneratorG()
- generatorH: Element, equivalent to G.GeneratorH()
def VerifyRangeProofHelper(verifier, D, nonceCommit, presentationLimit,
genGVar, genHVar):
bases = ComputeBases(presentationLimit)
num_bits = len(bases)
vars_b = []
vars_s = []
vars_s2 = []
vars_D = []
for i in range(num_bits):
# Append scalar variables without witness values
vars_b.append(verifier.AppendScalar("b" + str(i)))
vars_s.append(verifier.AppendScalar("s" + str(i)))
vars_s2.append(verifier.AppendScalar("s2" + str(i)))
# Append element variables for bit commitments D
vars_D.append(verifier.AppendElement("D" + str(i), D[i]))
# Add constraints proving each b[i] is in {0,1}
for i in range(num_bits):
# D[i] = b[i] * generatorG + s[i] * generatorH
verifier.Constrain(vars_D[i], [(vars_b[i], genGVar), (vars_s[i], genHVar)])
# D[i] = b[i] * D[i] + s2[i] * generatorH
verifier.Constrain(vars_D[i], [(vars_b[i], vars_D[i]), (vars_s2[i], genHVar)])
# Verify the sum check: nonceCommit == sum(bases[i] * D[i])
# This is done explicitly by computing the sum homomorphically
sum_D = G.Identity()
for i in range(len(bases)):
sum_D = sum_D + bases[i] * D[i]
sumValid = (sum_D == nonceCommit)
return (verifier, sumValid)
¶
A ciphersuite (also referred to as 'suite' in this document) for the protocol wraps the functionality required for the protocol to take place. The ciphersuite should be available to both the client and server, and agreement on the specific instantiation is assumed throughout.¶
A ciphersuite contains an instantiation of the following functionality:¶
Group: A prime-order Group exposing the API detailed in Section 3.1, with the
generator element defined in the corresponding reference for each group. Each
group also specifies HashToGroup, HashToScalar, and serialization functionalities.
For HashToGroup, the domain separation tag (DST) is constructed in accordance
with the recommendations in [I-D.irtf-cfrg-hash-to-curve], Section 3.1.
For HashToScalar, each group specifies an integer order that is used in
reducing integer values to a member of the corresponding scalar field.¶
This section includes an initial set of ciphersuites with supported groups. It also includes implementation details for each ciphersuite, focusing on input validation.¶
This ciphersuite uses P-256 [NISTCurves] for the Group. The value of the ciphersuite identifier is "P256". The value of contextString is "ARCV1-P256".¶
Group: P-256 (secp256r1) [NISTCurves]¶
Order(): Return 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551.¶
Identity(): As defined in [NISTCurves].¶
Generator(): As defined in [NISTCurves].¶
RandomScalar(): Implemented by returning a uniformly random Scalar in the range
[1, G.Order() - 1]. Refer to Section 6.2 for implementation guidance.¶
HashToGroup(x, info): Use hash_to_curve with suite P256_XMD:SHA-256_SSWU_RO_
[I-D.irtf-cfrg-hash-to-curve], input x, and DST =
"HashToGroup-" || contextString || info.¶
HashToScalar(x, info): Use hash_to_field from [I-D.irtf-cfrg-hash-to-curve]
using L = 48, expand_message_xmd with SHA-256, input x and
DST = "HashToScalar-" || contextString || info, and
prime modulus equal to Group.Order().¶
ScalarInverse(s): Returns the multiplicative inverse of input Scalar s mod Group.Order().¶
SerializeElement(A): Implemented using the compressed Elliptic-Curve-Point-to-Octet-String method according to [SEC1]; Ne = 33.¶
DeserializeElement(buf): Implemented by attempting to deserialize a 33-byte array to a public key using the compressed Octet-String-to-Elliptic-Curve-Point method according to [SEC1], and then performs partial public-key validation as defined in section 5.6.2.3.4 of [KEYAGREEMENT]. This includes checking that the coordinates of the resulting point are in the correct range, that the point is on the curve, and that the point is not the point at infinity. Additionally, this function validates that the resulting element is not the group identity element. If these checks fail, deserialization returns an InputValidationError error.¶
SerializeScalar(s): Implemented using the Field-Element-to-Octet-String conversion according to [SEC1]; Ns = 32.¶
DeserializeScalar(buf): Implemented by attempting to deserialize a Scalar from a 32-byte
string using Octet-String-to-Field-Element from [SEC1]. This function can fail if the
input does not represent a Scalar in the range [0, G.Order() - 1].¶
Two popular algorithms for generating a random integer uniformly distributed in the range [1, G.Order() -1] are as follows:¶
Generate a random byte array with Ns bytes, and attempt to map to a Scalar
by calling DeserializeScalar in constant time. If it succeeds and is non-zero,
return the result. Otherwise, try again with another random byte array, until the
procedure succeeds. Failure to implement DeserializeScalar in constant time
can leak information about the underlying corresponding Scalar.¶
As an optimization, if the group order is very close to a power of
2, it is acceptable to omit the rejection test completely. In
particular, if the group order is p, and there is an integer b
such that |p - 2b| is less than 2(b/2), then
RandomScalar can simply return a uniformly random integer of at
most b bits.¶
Generate a random byte array with L = ceil(((3 * ceil(log2(G.Order()))) / 2) / 8)
bytes, and interpret it as an integer; reduce the integer modulo G.Order() and return the
result. See [I-D.irtf-cfrg-hash-to-curve], Section 5 for the underlying derivation of L.¶
For arguments about correctness, unforgeability, anonymity, and blind issuance of the ARC protocol, see the "Formal Security Definitions for Keyed-Verification Anonymous Credentials" in [KVAC].¶
This section elaborates on unlinkability properties for ARC and other implementation details necessary for these properties to hold.¶
Client credential requests are constructed such that the server cannot distinguish between any two credential requests from the same client and two requests from different clients. We refer to this property as issuance unlinkability. This property is achieved by the way the credential requests are constructed. In particular, each credential request consists of two Pedersen commitments with fresh blinding factors, which are used to commit to a freshly generated client secret and request context. The resulting request is therefore statistically hiding, and independent from other requests from the same client. More details about this unlinkability property can be found in [KVAC] and [REVISITING_KVAC].¶
The server commitment to x0 is defined as X0 = x0 * G.generatorG() + x0Blinding * G.GeneratorH(), following the definitions in [KVAC]. This is computationally binding to the secret key x0. This means that unless the discrete log is broken, the credentials issued under one server commitment X0, X1, ... will all be issued under the same private keys x0, x1, ...¶
However, an adversary breaking the discrete log (e.g., a quantum adversary) can find pairs (x0, x0Blinding) and (x0', x0Blinding') both committing to X0 and use them to issue different credentials. This capability would let the adversary partitioning the client anonymity set by linking clients to the underlying secret used for credential issuance, i.e., x0 or x0'. This requires an active attack and therefore is not an immediate concern.¶
Statistical anonymity is possible by committing to x0 and x0Blinding separately, as in [REVISITING_KVAC]. However, the security of this construction requires additional analysis.¶
Client credential presentations are constructed so that all presentations are indistinguishable, even if coming from the same user. We refer to this property as presentation unlinkability. This property is achieved by the way the credential presentations are constructed. The presentation elements [U, UPrimeCommit, m1Commit] are indistinguishable from all other presentations made from credentials issued with the same server keys, as detailed in [KVAC].¶
The indistinguishability set for these presentation elements is sum_{i=0}^c(p_i), where c is the number of credentials issued with the same server keys, and p_i is the number of presentations made for each of those credentials.¶
The presentation elements [tag, nonceCommit, presentationContext, presentationProof, rangeProof] are indistinguishable from all presentations made from credentials issued with the same server keys for that presentationContext. The nonce is never revealed to the server since it is hidden within a Pedersen commitment. The range proof ensures the committed nonce is within the valid range [0, presentationLimit) without revealing its value. This provides strong unlinkability properties: the server cannot link presentations based on nonce values, as the nonce commitment uses a fresh random blinding factor for each presentation.¶
The indistinguishability set for these presentation elements is sum_{i=0}^c(p_i[presentationContext]), where c is the number of credentials issued with the same server keys and p_i[presentationContext] is the number of presentations made for each of those credentials with the same presentationContext. Unlike protocols where nonces are revealed, presentations can not be linked by comparing nonce values, resulting in maximum unlinkability within the presentation context.¶
To ensure no information is leaked during protocol execution, all operations that use secret data MUST run in constant time. This includes all prime-order group operations and proof-specific operations that operate on secret data, including proof generation and verification.¶
ARC uses the MACGGM algebraic MAC as its underlying primitive, as detailed in [KVAC] and [REVISITING_KVAC]. This offers the benefit of having a lower credential size than MACDDH, which is an alternative algebraic MAC detailed in [KVAC].¶
The BBS anonymous credential scheme, as detailed in [BBS] and its variants, is efficient and publicly verifiable, but requires pairings for verification. This is problematic for adoption because pairings are not supported as widely in software and hardware as non-pairing elliptic curves.¶
It is possible to construct a keyed-verification variant of BBS which doesn't use pairings, as discussed in [BBDT17] and [REVISITING_KVAC]. However these keyed-verification BBS variants require more analysis, proofs of security properties, and review to be considered mature enough for safe deployment.¶
This document has no IANA actions.¶
This section contains test vectors for the ARC ciphersuites specified in this document.¶
// ServerKey x0 = 3338fa65ec36e0290022b48eb562889d89dbfa691d1cde91517fa222ed7ad36 4 x1 = f9db001266677f62c095021db018cd8cbb55941d4073698ce45c405d1348b7b 1 x2 = 350e8040f828bf6ceca27405420cdf3d63cb3aef005f40ba51943c802687796 3 xb = fd293126bb49a6d793cd77d7db960f5692fec3b7ec07602c60cd32aee595dff d X0 = 0232b5e93dc2ff489c20a986a84757c5cc4512f057e1ea92011a26d3ad2c562 88d X1 = 03c413230a9bd956718aa46138a33f774f4c708d61c1d6400d404243049d4a3 1dc X2 = 02db00f6f8e6d235786a120017bd356fe1c9d09069d3ac9352cc9be10ef1505 a55 // CredentialRequest Blinding_0 = e699140babbe599f7dd8f6e3e8f615e5f201d1c2b0bc2f821f19e80 a0a0e1e7b Blinding_1 = b78e57df8f0a95d102ff12bbb97e15ed35c23e54f9b4483d30b7677 2ee60d886 Blinding_2 = 42252210dd60ddbbf1a57e3b144e26dd693b7644a9626a8c36896ed e53d12930 Blinding_3 = f5a4bbcf14e55e357df9f5ccb5ded37b2b14bc2e1a68e31f86416f0 606ee75d1 request_context = 74657374207265717565737420636f6e74657874 m1 = eedfe7939e2382934ab5b0f76aae44124955d2c5ebf9b41d88786259c34692d 2 m2 = 911fb315257d9ae29d47ecb48c6fa27074dee6860a0489f8db6ac9a486be6a3 e r1 = 008035081690bfde3b1e68b91443c22cc791d244340fe957d5aa44d7313740d f r2 = d59c5e6ff560cc597c2b8ca25256c720bceca2ab03921492c5e9e4ad3b55800 2 m1_enc = 03b8f11506a5302424143573e087fa20195cb5e893a67ef354eae3a78e2 63c54e4 m2_enc = 03f1ae4d7b78ba8030bd63859d4f4a909395c52bda34716b6620a2fdd52 b336fc9 proof = e098582f40a23bd1a402356ffe4df3928449babcfd7e3b53f8276e3e7fae cda39a9fdf6aada14315f651250f1374f308f13c9fd5c0e4278ee0d1e75940391e63 e0a7f63fb3fde10bdf14fb8ff2e760394e2456d736f01115c110a19b8cf2a885dd26 ed676e8c525ab9f89a358a86f1f6e668971b1e362000334681a24aa0f9c5b6882bb9 22dce84a23c2067e18c3c089c38eead30283e5d69dae36e5e57dc84d // CredentialResponse Blinding_0 = d7ed72750b6d366ed0febdc539b52d89434f468a578c59d7ca9015b 7da240ad6 Blinding_1 = 91eedfb168c556ff5ca3b89d047f482c9279b47f584aab6c7f895f7 674251771 Blinding_2 = 886ea898637537c9030595c187623157f9cc5c18da03fb49a5a36b7 e38849f3a Blinding_3 = 01df712887463dac146580d3357cb9dc02bdbddce141c952c094663 9ab515309 Blinding_4 = 2dc0d56bd8177de70c1883e08ceb0d712acbc3b36c950a756fbd80e b81718d85 Blinding_5 = 054049f980767d610023f178e0edb4a11a1dba90f1f93e39e95a720 e9d3c8d2d Blinding_6 = bf66d646c2b18dd604def14b6746314d91b4b49831ce8937dfd15ad 72be954e9 b = 95bcf45150a61f5c44a6cfbf343cd9e0f593f127b6f49bec0f9b20f0550504a2 U = 0349e4a381882bf7d005eec543bd3e1f2799458972086b91701d784e099c5f01 4b enc_U_prime = 0205f3224dafbce814bc740d69715ccc0b6d4bf52e9b983f13b4c3 4dcfd75cc86a X0_aux = 03583e734076fe750c26a211246a763be05ff4ad9c935d774bfe9b4f95f b4d886d X1_aux = 02ba0d01fb456fa0c7789878156b2a276c9379fb9f6c8c3bc25c3e05789 b00fb2c X2_aux = 030b3360e8250f1a8c3acc8f97fdc7c93c0371716b1c709ef6d4aaf54d5 43ea45e H_aux = 03f234f46994a880ee24ae235d70c01104f1959e71aa080fcf60d58af26a 54907f proof = 7a920ec29277897f132d93f4e5209cac5cd6a327f2c33fb4aaa58ae03e95 0d03c5823afdfc48940026d8d596163dc69878611402ae18156309aae26245d1fb4d 816c62558828bb7e2f7957553ac87e37387ff5ab294b478800d0c4f3948bc77a4c7a e3d7e37ce2288c01dd42b0dc41f87ee4b4e71602f57c5312d532169729250289f1ed e6a29fd5199c2dd5197e94730e5d1a0f655056c0c2ccacfd3b1df4519a4a5f0886ad 7dbdabfb158f26672f137fe1b5d95b476756342a35393e872dd4b50b86c6d82c75a4 12a76567caad14a817ec0438b6a6d9e07e79aa04aee6a90bf471e5363f6b30402d17 f075fc8b5bbc087abad9534b10146bb4205810a8d932 // Credential m1 = eedfe7939e2382934ab5b0f76aae44124955d2c5ebf9b41d88786259c34692d 2 U = 0349e4a381882bf7d005eec543bd3e1f2799458972086b91701d784e099c5f01 4b U_prime = 03f5b1db92ae68243a76d2b7bf720f52ebe20f940c29284d72e3043edb 1126e605 X1 = 03c413230a9bd956718aa46138a33f774f4c708d61c1d6400d404243049d4a3 1dc // Presentation1 Blinding_0 = e520caf9d5540af687c624b70298cf6526854a9c08b11946b07db49 6e4a006ce Blinding_1 = e66d65912d990848d4b1531fead70a2bb1b05d306d3bab855fa8dd5 63aa0b01c Blinding_2 = d9447d0cea53e8c6ef9b0f798d50e1cf31f0cd1315105ecf6bbf581 7d560571f Blinding_3 = 1b8d430f35fd99c6d8a666165c284049ccadc483584f4a4fbf2cc62 7a82783cd Blinding_4 = 3f8f4fda758b6ed060076d165492520fcbad558582837939baf99d5 2e8c7cd9c Blinding_5 = 409dd5280c655aaf9dbde20491a9c0324d51768b433dced9045dfa3 3800b216e Blinding_6 = 1733c8ea8c4af97e8e685dd5a9f74320a4cb1766d6da8fd90b358ce f2141635e Blinding_7 = 8fcd32796d29f6097200ae278ede1e3e38491cddd009deb3fb3f72a 27f018c76 presentation_context = 746573742070726573656e746174696f6e20636f6e746 57874 a = 9eae9ce883de40e99aa0fa098c1010fc57f58b8efaaf25f227459a4ddebfcba4 r = 2dfb016bfc2b35997b081777ac86c0bee2ea4018272f33aa6a79cc9f03af3cf9 z = f77f4f4ed706428db0abe222a617c57fdeb62e6214bd4d9ff5d27643072495cd U = 03e7989352bc30776e8654e1c91269257709186599ccbd5b0ec18116a562ac77 b6 U_prime_commit = 023ba477a2d790d03ad332d94a55407983bbb9e35366162e1b6 dcb376907534b6d m1_commit = 02f955c9ea4ad0f408c387f1e5c4841608f844e85e355e76f7fa3669 11c3bd60e0 nonce = 0x0 nonce_blinding = 5141094d4aa4e1a88364bafec2cf18928b72868633ee2229dd4 b3a58cf7756f3 nonce_commit = 02d50b2b283c32e006f8c16c6381811f3acd8aa8dc33e5be9e132 5a89c95b381ac tag = 031a774fd87a8f18f6420bea43cf5425e7426eec8ba7b8df5c13dc05f10ec6 52d9 D_0 = 02d50b2b283c32e006f8c16c6381811f3acd8aa8dc33e5be9e1325a89c95b3 81ac proof = 02d50b2b283c32e006f8c16c6381811f3acd8aa8dc33e5be9e1325a89c95 b381ace37ceecc5f57a815dd6fcc8d0bc35d074d17049fb05965d2e2bd2ba2496f16 24532b70c3108b18724116d391ff8bef2919545738d84d768e656e07aeaac49a9f50 6815376083a47e06ba0110cee2528f3bd814172c72a9237be04cddf44046d87a81a0 2604498c9bdcec69daba122af7d0373f8f1d6114dfbefafcac002f8e991b8d430f35 fd99c6d8a666165c284049ccadc483584f4a4fbf2cc627a82783cdd1e1fb2a9c21ed 1b05b754e0c7d36ab09fd986672b9b6a43cdc5ee30d07efe84409dd5280c655aaf9d bde20491a9c0324d51768b433dced9045dfa33800b216ea986743ab2e177c9341845 a01d385bc178f748487ff280e31e01ddcd08f89446221fddca93c0745317b095f202 1f36df4f8e5311d20a31391a51f8bd6a55980d // Presentation2 Blinding_0 = b68cd5b8b8d5115ad17f76297ed84c3169b1443abb42264b87e0a3a 1616fb816 Blinding_1 = 546bbc5ddad7950b4bb0bb3677376d3daf45eb7649285bbe6a15871 7365aea1d Blinding_2 = 05e51f42b2cc6706d105094825bb676babd0c0f04a644042711674c 15effb280 Blinding_3 = eeae9c7e13999d04c0694f9aa87b63b2a6e0d485b552939252f7e95 51da8d3cd Blinding_4 = c6847709cff95cc66269897532a55020a42e0b5ef9d8020e9c503f8 6d3436d3d Blinding_5 = be7ac96e0d07a97271563eb4f428701bb0fac3aa04c926637da21a7 63bdadc34 Blinding_6 = 757c14d0ed5c1bf1b622173b4e9e69cd558d2dcede9a76eef686a5e 89af62710 Blinding_7 = b1c0afa8e09abad10a4fea1a48542b9ffc5df785cb993d204de0a54 90ca6ff46 presentation_context = 746573742070726573656e746174696f6e20636f6e746 57874 a = cb6ac1b1658fd0edbe66db0b47489425824b0c2d2c8cae85012b9027995e55d9 r = a2756725298fdca322802a070e317e8ab7505bdec7a36c3529a0ee9260710061 z = e4b6f94a40102d58505660d7fea73947e3e621fda076076c3a22dd4ff4290b5b U = 02a3019dab1986921ac248e6260933b9c5adc390c0350eed5919770a9adcc0cc 2d U_prime_commit = 03e68da2708772d8b3f15c0ce862cf517de9be4c435db15869e f717464ffa39904 m1_commit = 028a0e1d3fdff513c49c64629baf1f3fa2d8c087fdfe632089a34495 7f78d26311 nonce = 0x1 nonce_blinding = 9209d3c54556821a00770474e9c9ebddb15a617423c002f121b 27df1c9f36b39 nonce_commit = 025661d97a7c203bc3a1d29eacb338cd0c36de4b7b865428791a6 0f9f0df1bc468 tag = 03084fe6fff0ecc7c33ef5c49b492dda38083f52e9a2b70b88f3d4b4ba7b50 afba D_0 = 025661d97a7c203bc3a1d29eacb338cd0c36de4b7b865428791a60f9f0df1b c468 proof = 025661d97a7c203bc3a1d29eacb338cd0c36de4b7b865428791a60f9f0df 1bc468da7f0320982023f95d1e8f5ae2e335ddef80eb007ecb8d7d254d09a31b782e e60fa387001e9ab16ad240eb013d07c37332867843eebc57b188cca6eff3b8203290 d181f837af1283579f9bc69cb42fe49fe952639fb6c932449309d21ae36a7468592b f9948bf9ee06a7901f707190efdd18e97b1a7c70a6ce8b7d8a78554dc1142f995d7b 79790b634ac03fc5982dd4b75fe985368706152daadfb20230a4e72768c1e85988c6 4f1b082f203d40e91d1429961a668cf32cdff270c333e35c21e3fbc64c74e7857a14 37af5a11453a3d7e60d3572d15376b4c0edb961cc5d29fd6605fae76eb857b6ec0bc e6593a02c9826fb337f26706922de2a1e7f7f93b45b1c0afa8e09abad10a4fea1a48 542b9ffc5df785cb993d204de0a5490ca6ff46¶
The authors would like to acknowledge helpful conversations with Tommy Pauly about rate limiting and Privacy Pass integration, as well as Lena Heimberger for specifying the range proof.¶