| Internet-Draft | MoLE Cryptography | August 2026 |
| Doe | Expires 22 February 2027 | [Page] |
This document specifies the cryptographic construction used to produce and consume MoLE Endorsements. An Endorsement is an anonymous token that an Anchor issues to a Client, and that the Client later redeems at a Moderator without the Anchor being able to link the redemption to the issuance.¶
This document defines the endorsement issuance protocol, built from a pairing-free partially blind signature scheme, together with the group, encoding, and context-binding rules that both the Anchor and the Client follow.¶
This note is to be removed before publishing as an RFC.¶
The latest revision of this draft can be found at https://moderation-of-unlinkable-endorsements.github.io/internet-drafts/draft-authors-mole-cryptography.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-authors-mole-crypto/.¶
Source for this draft and an issue tracker can be found at https://github.com/Moderation-of-unLinkable-Endorsements/internet-drafts.¶
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 22 February 2027.¶
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.¶
MoLE Endorsements have a number of constraints imposed by the architecture
[ARCH]. They must be unlinkable by the Anchor that issued them, they must be
publicly verifiable, and a redemption must hide which Anchor issued the
Endorsement among the set of Anchors a Moderator accepts. Existing systems do
not meet all of these needs. This document defines such a system, the
Issuer-Hiding Anonymous Token (IHAT), which is endorsement type 0x0002 in
[PROTOCOLS].¶
The construction is a pairing-free partially blind signature [TESSZHU]. An Anchor holds a signing key and issues, in three moves, a signature on a Client-chosen message that the Anchor never sees. Each Endorsement is also bound at issuance time to two contexts. These may be used to limit the validity scope of each Endorsement, i.e., when and for whom it may later be used.¶
The issuance context ctx_iss is agreed out of band among the Client, the
Anchors, and the Moderator. ctx_iss can encode, for instance, the time
period in which the Endorsement is issued, allowing us to capture Endorsement
expiry.¶
The redemption context ctx_red is agreed out of band between the Client
and the Moderator. For example, it may be a long-term identity of the target
Moderator. This may be used to prevent Endorsement reuse across Moderators
without requiring a synchronized state between them.¶
This document is a work in progress. This revision specifies:¶
the protocol context, scalar derivation, Anchor key generation, and context binding (Section 4);¶
the endorsement issuance protocol, that is, the four algorithms Commit,
Challenge, Respond, and Finalize, along with the wire messages they
exchange, and the endorsement verification equation (Section 5);¶
The following are not yet specified and are marked as such in the text:¶
endorsement redemption, including the issuer-hiding proof over a Moderator's Anchor Set (Section 6);¶
test vectors (Appendix A).¶
Editorial note. [PROTOCOLS] currently names the grant functions
Prepare, Sign, RequestProof, Prove, and Finalize, which assume the
Client sends the first message. In the construction specified here the Anchor
sends the first message, so the algorithms are named Commit, Challenge,
Respond, and Finalize. TODO: rename these in [PROTOCOLS]. The number
of HTTP exchanges (two) and the endorsement type are unchanged.¶
Editorial note. This document binds an Endorsement to two contexts, an
issuance context and a redemption context (Section 4.4), whereas
[PROTOCOLS] currently defines only a single endorsement_context.
TODO: [ARCH] is expected to define both contexts and to fix their
encodings; until it does, this document treats them as opaque byte strings and
gives only non-normative examples. [PROTOCOLS] is then to be updated to
match.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
The terms Client, Anchor, Moderator, Endorsement, Credential, and Anchor Set are used as defined in [ARCH].¶
Unless otherwise specified, this document encodes protocol messages in TLS notation (Section 3 of [TLS13]). Moreover, all constants are in network byte order.¶
The following functions and notation are used throughout this document.¶
For any byte string x, len(x) denotes its length in bytes.¶
For two byte strings x and y, x || y denotes their concatenation.¶
For a byte string x, x[i..j] denotes the substring of x that begins at
its byte with index i and ends just before its byte with index j, where
indices start at zero. Its length is j - i bytes.¶
I2OSP(x, xLen) converts a nonnegative integer x into a byte string of
length xLen in big-endian byte order, as described in [I2OSP].¶
random(n) returns n uniformly random bytes. Implementations MUST generate
them with a cryptographically secure random number generator. It is the only
source of randomness in this document: every other value that has to be
unpredictable is derived from its output (Section 4.2).¶
String values in monospace and quotes, such as "Challenge", are ASCII string
literals and do not include a terminating NUL byte.¶
All algorithms are laid out in Python-like pseudocode. Each algorithm takes a set of inputs and parameters and produces a set of outputs. Parameters become constant values once the ciphersuite is fixed. An algorithm that can fail raises an error; the errors used in this document are listed in Section 3.2.¶
The construction has two dependencies:¶
A prime-order group implementing the interface in Section 3.1. Section 7 gives concrete instances.¶
A cryptographic hash function whose output length is Nh bytes.¶
This document uses an additive, prime-order group, denoted G, of order p,
as described in Section 2.1 of [OPRF]. The types Element and Scalar
denote elements of the group and of its scalar field respectively. Group
elements are added with + and subtracted with -; scalar multiplication of
an Element A by a Scalar r is written r * A. Scalars are added,
subtracted, and multiplied modulo p.¶
The following member functions are used. Except where noted, they are as defined in Section 2.1 of [OPRF].¶
Outputs the order p of the group.¶
Outputs the identity element of the group.¶
Outputs the generator element B of the group.¶
Outputs r * B, where B is the group generator.¶
Deterministically maps a byte string x to an Element. Parameterized by a
domain separation tag (DST); see Section 7.¶
Deterministically maps a byte string x to a Scalar. Parameterized by a
DST; see Section 7.¶
Outputs the multiplicative inverse of the nonzero Scalar s modulo p.¶
Maps an Element A to a canonical byte string of fixed length Ne.¶
Attempts to map a byte string buf to an Element. Raises a
DeserializeError if buf is not the canonical encoding of a group element,
or if it encodes the identity element.¶
Maps a Scalar s to a canonical byte string of fixed length Ns.¶
Attempts to map a byte string buf to a Scalar. Raises a
DeserializeError if buf does not encode a Scalar in the range
[0, p-1].¶
This document does not use the RandomScalar() member of
Section 2.1 of [OPRF]. Every scalar that has to be unpredictable is instead
obtained from DeriveScalar (Section 4.2), which is deterministic in a
random seed. This makes each algorithm reproducible from the seed it is given,
which is what allows the test vectors of Appendix A to pin the randomness
of an otherwise randomized protocol.¶
The following errors are used.¶
A received byte string is not a valid encoding of the expected type.¶
A received value failed a verification check.¶
A message was received for a session that is not in the expected state.¶
A deterministic derivation from a seed failed to produce a usable value. See Section 4.2.¶
An implementation that raises an error MUST abort the protocol run. Errors are fatal to the affected session; see Section 5.7.¶
Issuance is a three-move protocol between a Client and an Anchor, followed by a local finalization step at the Client. The Anchor moves first and holds per-session state between its two moves.¶
Client(pkA, ctx_iss, ctx_red) Anchor(skA, ctx_iss)
---------------------------------------------------------------------
state, commitment = Commit(skA, ctx_iss)
commitment
<--------
state, challenge = Challenge(pkA, ctx_iss, ctx_red, commitment)
challenge
-------->
response = Respond(skA, state, challenge)
response
<--------
endorsement = Finalize(pkA, state, response)
The Anchor speaks first. This document does not say how the three messages are carried, nor how a Client that wants an Endorsement reaches an Anchor in the first place; both are the business of the transport, and a Client will in general have to signal its intent by some means that carries no protocol data. For example, over HTTP a Client might ask for issuance in a request with an empty body, receive the commitment in the response, send the challenge in a second request, and receive the response in the reply to that. Section 5.6 specifies the encoding of the three messages and their mapping onto the exchanges of [PROTOCOLS].¶
Neither context is carried in these messages. Both parties already hold the issuance context, having agreed on it out of band; the redemption context is known only to the Client.¶
The Client's output is an Endorsement that is publicly verifiable under the
Anchor's public key pkA (Section 5.5). The Anchor learns neither the nullifier
nor the redemption context bound into it, and cannot link the Endorsement to
the session that produced it.¶
A ciphersuite (Section 7) is identified by an ASCII string
identifier. Both parties MUST agree on the ciphersuite before running the
protocol; [PROTOCOLS] describes how this agreement is reached.¶
The protocol context, written ctx_proto, is the domain separation tag that
this document derives from that identifier:¶
def CreateProtocolContext(identifier): return "IHATv1-" || identifier¶
Throughout the remainder of this document, ctx_proto denotes the output of
CreateProtocolContext for the ciphersuite in use. It is distinct from the
issuance and redemption contexts of Section 4.4: those are inputs to
the protocol, chosen by its participants, whereas ctx_proto is fixed by the
ciphersuite.¶
Every hash this document computes is domain-separated by ctx_proto, which it
carries in its DST rather than in its input: HashToGroup and HashToScalar
are so parameterized (Section 7), and so is DeriveScalar
(Section 4.2). Every algorithm below therefore depends on ctx_proto,
including those in which it does not appear explicitly, and a value produced
under one ciphersuite does not verify under another. Each algorithm lists
ctx_proto among its parameters where it has this dependence.¶
Scalars that have to be unpredictable are not sampled directly. They are derived from a random seed, so that an algorithm is a deterministic function of the seed it is given:¶
Input:¶
opaque seed[Nseed] PublicInput info¶
Output:¶
Scalar s¶
Parameters:¶
Group G PublicInput ctx_proto¶
Errors: DeriveError¶
def DeriveScalar(seed, info):
derive_input = seed || I2OSP(len(info), 2) || info
counter = 0
s = 0
while s == 0:
if counter > 255:
raise DeriveError
s = G.HashToScalar(derive_input || I2OSP(counter, 1),
DST = "DeriveScalar-" || ctx_proto)
counter = counter + 1
return s
¶
The output is never zero, so a caller that needs a nonzero scalar needs no
further check. The loop terminates after one iteration except with probability
approximately 1/p, and DeriveError is raised only if 256 consecutive
iterations yield zero; neither is expected to be observed.¶
A seed MUST be Nseed bytes of random output, and MUST NOT be used for more
than one derivation. An algorithm that needs several scalars therefore draws
Nseed bytes for each of them, and additionally separates them by info.
Nseed is larger than Ns (Section 7) so that the derived scalar is
statistically close to uniform rather than merely unpredictable; deriving
several scalars from one seed instead would cap their joint entropy at the
length of that seed, which the unlinkability argument of
Section 8 does not permit. See Section 7.3.¶
An Anchor holds a key pair (skA, pkA). It is derived from a seed, which is
what allows the test vectors in Appendix A to fix a key. The procedure is
the key generation of Section 3.2 of [OPRF]. Note that, by design, knowledge
of both seed and info is required, so the secrecy of skA rests on the
secrecy of seed; info is public.¶
Input:¶
opaque seed[Nseed] PublicInput info¶
Output:¶
Scalar skA Element pkA¶
Parameters:¶
Group G PublicInput ctx_proto¶
Errors: DeriveError¶
def DeriveKeyPair(seed, info): skA = DeriveScalar(seed, info) pkA = G.ScalarMultGen(skA) return (skA, pkA)¶
The derivation is the one Section 3.2 of [OPRF] performs inline: hash
seed || I2OSP(len(info), 2) || info together with a counter, rejecting zero
(Section 4.2). It differs only in its domain separation tag, which comes
from the protocol context of this document rather than from an OPRF context
string, and in the length of the seed.¶
A fresh key pair is generated by deriving one from a random seed.¶
Input:¶
None¶
Output:¶
Scalar skA Element pkA¶
Parameters:¶
Group G PublicInput ctx_proto¶
Errors: DeriveError¶
def GenerateKeyPair(): seed = random(Nseed) return DeriveKeyPair(seed, "GenerateKeyPair")¶
The Anchor publishes SerializeElement(pkA) in its configuration; see
[PROTOCOLS].¶
Each Endorsement is bound at issuance to two contexts, the issuance context and
the redemption context, and a redemption succeeds only if the Client and the
Moderator agree on both values. Both are opaque byte strings of at most
2^16 - 1 bytes, a bound that follows from the two-byte length prefixes used
below. The two are bound by deliberately different means, reflecting who is
trusted to choose each.¶
TODO. [ARCH] is expected to fix what these two byte strings contain. Until then this document treats them as opaque, and the examples below are illustrative only, not normative.¶
The issuance context, written ctx_iss, restricts when an Endorsement may be
redeemed; it might for example name the epoch the Endorsement was issued in.
Both parties hold it. It is bound by deriving the second commitment base from
it:¶
def CreateContextBase(ctx_iss):
context_base_input =
I2OSP(len(ctx_iss), 2) || ctx_iss ||
"ContextBase"
return G.HashToGroup(context_base_input)
¶
The Anchor forms its commitment under this base, and the base is recomputed at
verification time. The Client and the Anchor MUST agree on the issuance context.
Disagreement causes issuance to fail: a Client that uses any other value fails
the commitment-opening check in Finalize. The binding is therefore enforced by
the construction rather than by an explicit check, and a Client cannot bind an
Endorsement to an issuance context of its own choosing.¶
The redemption context, written ctx_red, restricts where an Endorsement
may be redeemed: a redemption succeeds only under the value the Endorsement was
issued under. It might for example identify the Moderator the Client intends to
redeem at, in which case the Endorsement is redeemable at that Moderator and at
no other. It is chosen by the Client and is hidden from the Anchor. It is bound
by placing it, together with a fresh Client-chosen nullifier nf of Nn = 32
bytes, in the signed message:¶
def Message(nf, ctx_red):
return I2OSP(len(nf), 2) || nf ||
I2OSP(len(ctx_red), 2) || ctx_red
¶
A redemption under a different redemption context recomputes a different
message, for which the Client holds no valid signature. Two consequences
follow. A Client has to fix ctx_red before it runs Challenge, that is,
before the Endorsement exists; and an Endorsement cannot afterwards be re-bound
to another value, so a Client that needs to redeem under several redemption
contexts needs a separate Endorsement, and so a separate issuance, for each.
Anchors bound how many Endorsements they grant a given Client in order to keep
Endorsements scarce ([ARCH]), so that budget is consumed per redemption
context rather than per Client.¶
The nullifier nf MUST be a fresh string of Nn uniformly random bytes,
generated by the Client, and MUST NOT be reused across Endorsements. It is
revealed at redemption, where the Moderator uses it to enforce that each
Endorsement is redeemed at most once.¶
The values the two contexts take determine the anonymity set a Client redeems in, and a deployment can destroy the unlinkability the construction provides without breaking any of its cryptographic properties; see Section 8.¶
Issuance produces a signature on the message Message(nf, ctx_red) relative to
the public input ctx_iss. It consists of four algorithms, run in the order¶
Commit -> Challenge -> Respond -> Finalize¶
Commit and Respond are run by the Anchor; Challenge and Finalize are
run by the Client. Both parties input the issuance context ctx_iss; only the
Client inputs the redemption context ctx_red.¶
Each of the first three algorithms outputs one protocol message, and the next
algorithm takes that message as a single input. The messages are the
commitment, the pair (A, C); the challenge, a single scalar; and the
response, the triple (s, y, t). The types Commitment and Response denote
the first and the last of these. The wire format of each message is defined in
Section 5.6.¶
All four algorithms take the group G as a parameter, and all of them except
Respond, which computes no hash, take the protocol context ctx_proto
(Section 4.1). Parameters are listed with each algorithm and are omitted from
the argument lists in the pseudocode.¶
The Anchor opens a session by committing to the values it will later reveal.¶
Input:¶
Scalar skA PublicInput ctx_iss¶
Output:¶
AnchorState state Commitment commitment¶
Parameters:¶
Group G PublicInput ctx_proto Nseed¶
Errors: DeriveError¶
def Commit(skA, ctx_iss): Z = CreateContextBase(ctx_iss) rand = random(3 * Nseed) a = DeriveScalar(rand[0 .. Nseed], "a") t = DeriveScalar(rand[Nseed .. 2 * Nseed], "t") y = DeriveScalar(rand[2 * Nseed .. 3 * Nseed], "y") A = G.ScalarMultGen(a) C = G.ScalarMultGen(t) + y * Z state = (a, y, t) commitment = (A, C) return state, commitment¶
The Anchor stores state for the duration of the session and sends commitment
to the Client in a CommitMessage (Section 5.6). Note that skA is not used in
Commit; it appears in the interface because an implementation MAY choose to
carry it in the session state rather than reload it in Respond.¶
Commit draws all of its randomness in one call and splits it into one seed
per scalar (Section 4.2). A test vector fixes the single value rand;
y is nonzero by construction.¶
The Client blinds the Anchor's commitment, derives the challenge over the blinded values, and returns the challenge in blinded form.¶
Input:¶
Element pkA PublicInput ctx_iss PrivateInput ctx_red Commitment commitment¶
Output:¶
ClientState state Scalar challenge¶
Parameters:¶
Group G PublicInput ctx_proto Nn Nseed¶
Errors: VerifyError, DeriveError¶
def Challenge(pkA, ctx_iss, ctx_red, commitment):
(A, C) = commitment
rand = random(Nn + 4 * Nseed)
nf = rand[0 .. Nn]
seeds = rand[Nn .. Nn + 4 * Nseed]
r1 = DeriveScalar(seeds[0 .. Nseed], "r1")
r2 = DeriveScalar(seeds[Nseed .. 2 * Nseed], "r2")
gamma1 = DeriveScalar(seeds[2 * Nseed .. 3 * Nseed], "gamma1")
gamma2 = DeriveScalar(seeds[3 * Nseed .. 4 * Nseed], "gamma2")
m = Message(nf, ctx_red)
gamma = gamma1 * G.ScalarInverse(gamma2)
blinded_A = G.ScalarMultGen(r1) + gamma * A
blinded_C = gamma1 * C + G.ScalarMultGen(r2)
blinded_commitment = (blinded_A, blinded_C)
c = ComputeChallenge(ctx_iss, blinded_commitment, m)
if c == 0:
raise VerifyError
challenge = c * gamma2
state = (nf, ctx_iss, ctx_red, commitment,
r1, r2, gamma1, gamma2, challenge, c)
return state, challenge
¶
As in Commit, all randomness is drawn in one call and split: the first Nn
bytes are the nullifier, and the remaining 4 * Nseed bytes are four seeds,
one per blinding scalar. ComputeChallenge is as follows.¶
def ComputeChallenge(ctx_iss, commitment, m):
(A, C) = commitment
Am = G.SerializeElement(A)
Cm = G.SerializeElement(C)
challenge_transcript =
I2OSP(len(ctx_iss), 2) || ctx_iss ||
I2OSP(len(Am), 2) || Am ||
I2OSP(len(Cm), 2) || Cm ||
I2OSP(len(m), 2) || m ||
"Challenge"
c = G.HashToScalar(challenge_transcript)
return c
¶
Two challenge values appear here and it matters which is which. c is computed
over the blinded commitment and is the value that ends up in the Endorsement
(Section 5.4); it never leaves the Client. The challenge message the Anchor
receives is its blinded form c * gamma2, and the Anchor cannot recover c
from it because gamma2 is uniform and secret.¶
HashToScalar can return zero, whereas the construction requires a nonzero
challenge. Challenge therefore aborts in that case rather than resampling, so
that the challenge remains a deterministic function of the transcript. The
abort occurs with probability approximately 1/p and is not expected to be
observed in practice; see Section 8.¶
The Client sends challenge to the Anchor in a ChallengeMessage (Section 5.6)
and retains state.¶
The Anchor answers the challenge and closes the session.¶
Input:¶
Scalar skA AnchorState state Scalar challenge¶
Output:¶
Response response¶
Errors: VerifyError, SessionError¶
def Respond(skA, state, challenge):
(a, y, t) = state
if challenge == 0:
raise VerifyError
s = a + challenge * y * skA
response = (s, y, t)
return response
¶
An Anchor MUST call Respond at most once per session state produced by
Commit, and MUST destroy that state immediately afterwards. Answering two
distinct challenges on the same state discloses the signing key: from
s1 = a + c1 * y * skA and s2 = a + c2 * y * skA with c1 != c2, and y
revealed in the response, an attacker recovers
skA = (s1 - s2) * ScalarInverse((c1 - c2) * y). An Anchor that receives a
second ChallengeMessage for a session it has already answered MUST raise a
SessionError and MUST NOT compute a response.¶
The Client checks the Anchor's response and unblinds it into an Endorsement.¶
Input:¶
Element pkA ClientState state Response response¶
Output:¶
Endorsement endorsement¶
Parameters:¶
Group G PublicInput ctx_proto¶
Errors: VerifyError¶
def Finalize(pkA, state, response):
(nf, ctx_iss, ctx_red, commitment,
r1, r2, gamma1, gamma2, challenge, c) = state
(A, C) = commitment
(s, y, t) = response
Z = CreateContextBase(ctx_iss)
if y == 0:
raise VerifyError
if C != G.ScalarMultGen(t) + y * Z:
raise VerifyError
if G.ScalarMultGen(s) != A + (challenge * y) * pkA:
raise VerifyError
gamma = gamma1 * G.ScalarInverse(gamma2)
s_final = gamma * s + r1
y_final = gamma1 * y
t_final = gamma1 * t + r2
return Endorsement(c, s_final, y_final, t_final, nf)
¶
The three checks verify that the Anchor opened its commitment honestly and
answered the challenge under its published key. A Client whose Finalize
raises an error MUST discard the session state and MUST NOT retry the exchange
with the same state; it may start a fresh session.¶
An Endorsement consists of the signature (c, s, y, t) together with the
nullifier; its encoding is given in Section 5.6.3. The contexts it
is bound to are not part of it. They are inputs to verification, supplied by
the verifier, and a Client keeps its own copy of them for as long as it holds
the Endorsement.¶
The Endorsement is held privately by the Client until it is redeemed (Section 6). The Anchor never sees it.¶
An Endorsement is publicly verifiable under the issuing Anchor's public key. Verification requires no interaction with the Anchor and no pairing.¶
The two contexts are inputs to Verify, not fields of the Endorsement. A
verifier therefore states the pair it is willing to accept and learns whether
the Endorsement was issued under it, rather than being told by the Client which
pair to check against.¶
Input:¶
Element pkA Endorsement endorsement PublicInput ctx_iss PublicInput ctx_red¶
Output:¶
boolean verified¶
Parameters:¶
Group G PublicInput ctx_proto¶
def Verify(pkA, endorsement, ctx_iss, ctx_red):
(c, s, y, t, nf) = endorsement
if y == 0 or c == 0:
return false
Z = CreateContextBase(ctx_iss)
m = Message(nf, ctx_red)
C = G.ScalarMultGen(t) + y * Z
A = G.ScalarMultGen(s) - (c * y) * pkA
commitment = (A, C)
return c == ComputeChallenge(ctx_iss, commitment, m)
¶
Verify is stated here for completeness and for use in test vectors. A
Moderator does not call it directly: a redemption does not reveal which Anchor
issued the Endorsement, so the check is instead carried out under an
issuer-hiding proof (Section 6). The Client MUST NOT reveal the Anchor's
public key to the Moderator.¶
An honestly produced Endorsement always verifies. Writing gamma for
gamma1 * ScalarInverse(gamma2), and A_anchor and C_anchor for the two
elements of the Anchor's commitment, the commitment reconstructed by Verify is
exactly the blinded commitment the Client hashed in Challenge:¶
C = t_final * B + y_final * Z
= gamma1 * (t * B + y * Z) + r2 * B
= gamma1 * C_anchor + r2 * B
= blinded_C
A = s_final * B - (c * y_final) * pkA
= (gamma * s + r1) * B - (c * gamma1 * y) * pkA
= r1 * B + gamma * A_anchor
= blinded_A
¶
where the last step uses s = a + challenge * y * skA and
challenge = c * gamma2, so that the two terms in skA cancel.¶
This section gives the encoding of the three messages exchanged during issuance, of the session identifier that correlates them, and of the Endorsement they produce.¶
Element and Scalar are the fixed-length encodings produced by
SerializeElement and SerializeScalar, of Ne and Ns bytes respectively.
A recipient MUST deserialize every received Element and Scalar, and MUST
abort the session with a DeserializeError if deserialization fails. In
particular, deserializing an Element rejects the group identity element.¶
The three messages exchanged during issuance are carried in the
EndorsementRequest and EndorsementResponse bodies defined in [PROTOCOLS],
whose transport is HTTP.¶
Issuance has three messages, but the Anchor sends the first of them, and HTTP is
client-initiated. The Client therefore opens the session with a request whose
body is empty: it is a trigger, not a protocol message, and the three moves
of Section 4 are executed after it. The "Exchanges" field of the IHAT
registration in [PROTOCOLS] counts HTTP exchanges, of which there are still
two. Neither context appears on the wire (Section 4.4).¶
| Exchange | Request body | Response body |
|---|---|---|
| 1 | empty |
CommitMessage
|
| 2 |
ChallengeMessage
|
ResponseMessage
|
An Anchor MUST reject a first request whose body is non-empty. There is
nothing for a Client to say in it, and accepting Client-supplied bytes there
would invite a Client to try to influence the issuance context.¶
The Anchor opens the session with its commitment, whose two elements are carried as separate fields:¶
struct {
opaque session_id<V>;
Element A;
Element C;
} CommitMessage;
¶
The Client replies with the blinded challenge, echoing the session identifier:¶
struct {
opaque session_id<V>;
Scalar challenge;
} ChallengeMessage;
¶
The Anchor replies with its response, which closes the session:¶
struct {
Scalar s;
Scalar y;
Scalar t;
} ResponseMessage;
¶
The Anchor holds secret state between its two moves (Section 5.7), so the two
exchanges have to be correlated. session_id does that. It is generated by
the Anchor, opaque to the Client, and echoed unmodified in ChallengeMessage.¶
An Anchor MUST NOT have two open sessions with the same session_id, and SHOULD
generate it with a cryptographically secure random number generator so that a
Client cannot guess, and so collide with, another Client's session. An Anchor
that receives a ChallengeMessage whose session_id does not correspond to one
of its open sessions MUST raise a SessionError.¶
The session identifier MUST NOT be bound into the challenge transcript, and it is not an input to any algorithm in Section 5. It is a value the Anchor chose and therefore recognises; anything the Anchor recognises that also reached the Endorsement would let it link a redemption back to the issuance session, which is exactly the property the construction exists to prevent. It is transport bookkeeping only.¶
Correlating by connection reuse instead was considered and is not specified. It does not survive deployments in which the Client has no stable connection to the Anchor, including the use of Oblivious HTTP, which [ARCH] contemplates.¶
The output of Finalize is encoded as follows.¶
struct {
Scalar c;
Scalar s;
Scalar y;
Scalar t;
opaque nf[Nn];
} Endorsement;
¶
This structure is never sent to an Anchor. The Client holds it until it is
redeemed, and Section 6 defines what is sent to a Moderator then. The
issuance and redemption contexts are not carried in it: they are inputs to
Verify (Section 5.5) and to redemption, held by the verifier.¶
An Anchor is stateful: it holds the secret state produced by Commit from the
moment it sends CommitMessage until it answers or discards the session. That
state is single-use; see Section 5.3 and Section 8.¶
An Anchor SHOULD bound both the number of concurrent open sessions per Client
and the lifetime of an open session, and SHOULD discard state for sessions that
are not completed within that lifetime. Discarding state early is always safe:
it causes the Client's Finalize to be unreachable, but cannot produce an
invalid Endorsement.¶
TODO. This section is not yet written. It will specify how a Client
redeems an Endorsement at a Moderator, that is, the Present and Verify
functions that [PROTOCOLS] refers to. This document calls the operation
redemption, following [ARCH], which reserves Presentation for the
Credential flow; the message a Client sends is nonetheless the Presentation
of [PROTOCOLS]. It needs to cover at least:¶
Issuer hiding. The construction above is verifiable under a single Anchor key, so redeeming it as-is would reveal the issuing Anchor. The intended approach is for the Client to present the signature under a rerandomized key and prove, in zero knowledge, that the rerandomized key corresponds to some key in the Moderator's Anchor Set: a 1-of-n OR proof composed from Sigma protocols [SIGMA]. The Anchor Set is ordered, and proof branches are matched to keys by position ([PROTOCOLS]).¶
Challenge binding. [PROTOCOLS] requires that a redemption be bound to
the challenge_digest of the challenge that triggered it, and that
verification fail under any other digest. Note that this challenge is the
Moderator's, and is unrelated to the issuance challenge of Section 5.2.¶
What the Moderator learns. Both contexts are inputs to verification
(Section 5.5), so a Moderator states the pair it accepts rather than being
told it by the Client; a Client whose Endorsement was issued under any other
pair simply fails. The redemption reveals nf, which the Moderator checks
it has not seen before.¶
A ciphersuite fixes the group, the hash functions, and the associated encodings and domain separation tags. Both parties are assumed to agree on the ciphersuite in use (Section 4.1).¶
For each ciphersuite, ctx_proto is as computed in Section 4.1. The nullifier
length is Nn = 32 bytes and the seed length is Nseed = Ns + 16 bytes, that
is 48 bytes, for both ciphersuites below. The 16 bytes in excess of Ns are
what makes a derived scalar statistically close to uniform (Section 4.2),
on the same grounds that [HASH2CURVE] oversamples by 128 bits when it maps a
byte string to a field element.¶
This ciphersuite uses P-256 (secp256r1) [NISTCurves] for the group and
SHA-256 for the hash function, with Nh = 32. The value of the ciphersuite
identifier is "P256-SHA256".¶
The interface of Section 3.1 is instantiated as follows.¶
Return 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551.¶
As defined in [NISTCurves].¶
Use hash_to_curve with suite P256_XMD:SHA-256_SSWU_RO_ [HASH2CURVE] and
DST = "HashToGroup-" || ctx_proto.¶
Use hash_to_field from [HASH2CURVE] with L = 48, expand_message_xmd
with SHA-256, DST = "HashToScalar-" || ctx_proto, and a prime modulus
equal to Order().¶
The multiplicative inverse of s modulo Order().¶
The compressed Elliptic-Curve-Point-to-Octet-String method of [SEC1];
Ne = 33.¶
Deserialize a 33-byte input using the compressed
Octet-String-to-Elliptic-Curve-Point method of [SEC1], then perform partial
public key validation as in Section 4.3 of [OPRF]. This includes checking
that the coordinates are in range, that the point is on the curve, and that
the point is not the identity element. Raise a DeserializeError if any
check fails.¶
The Field-Element-to-Octet-String conversion of [SEC1]; Ns = 32.¶
Deserialize a 32-byte input using Octet-String-to-Field-Element from
[SEC1]. Raise a DeserializeError if the result is not in
[0, Order()-1].¶
This ciphersuite uses ristretto255 [RISTRETTO] for the group and SHA-512 for
the hash function, with Nh = 64. The value of the ciphersuite identifier is
"ristretto255-SHA512".¶
The interface of Section 3.1 is instantiated as follows.¶
Return 2^252 + 27742317777372353535851937790883648493, as defined in [RISTRETTO].¶
Use hash_to_ristretto255 [HASH2CURVE] with
DST = "HashToGroup-" || ctx_proto and expand_message_xmd using
SHA-512.¶
Compute uniform_bytes using expand_message_xmd with SHA-512,
DST = "HashToScalar-" || ctx_proto, and an output length of 64 bytes;
interpret uniform_bytes as a 512-bit integer in little-endian order and
reduce it modulo Order().¶
The multiplicative inverse of s modulo Order().¶
The Encode function of Section 4.3.2 of [RISTRETTO]; Ne = 32.¶
The Decode function of Section 4.3.1 of [RISTRETTO], additionally
validating that the result is not the identity element. Raise a
DeserializeError if any check fails.¶
The little-endian 32-byte encoding of the Scalar, with the top three bits
set to zero; Ns = 32.¶
Deserialize a Scalar from a little-endian 32-byte string. Raise a
DeserializeError if the result is not in [0, Order()-1]; note that this
requires the top three bits of the input to be zero.¶
Every random value in this document is a seed of Nseed bytes, drawn with
random and consumed by DeriveScalar (Section 4.2); no scalar is
sampled directly. Implementations MUST draw seeds with a cryptographically
secure random number generator and MUST NOT reuse a seed across derivations.
They SHOULD treat a seed as being as sensitive as the values derived from it,
and SHOULD handle both in constant time: the seed drawn in Commit determines
the Anchor's session state, and the seed drawn in Challenge determines the
Client's blinding factors, so recovering either undoes the property that
algorithm provides.¶
Editorial note. This document does not use the RandomScalar() member of
[OPRF]'s group interface, and so does not have to resolve an inconsistency
in it: Section 2.1 of [OPRF] defines RandomScalar() as "Chooses at random
a nonzero element in GF(p)", while each of its ciphersuites defines it as "a
uniformly random Scalar in the range [0, G.Order() - 1]", which includes
zero. TODO: report the inconsistency against [OPRF].¶
TODO. This section is a summary of the properties the construction is intended to provide and of the requirements implementations must meet. Formal statements, the corresponding reductions, and the treatment of endorsement redemption are not yet written.¶
The issuance protocol is the partially blind signature scheme of Tessaro and Zhu [TESSZHU], instantiated with the public input set to the issuance context. Its security is analysed in the random oracle model, and one-more unforgeability additionally in the algebraic group model under the discrete logarithm assumption. Notably, its concurrent security does not rely on the hardness of the ROS problem, which is broken in polynomial time, nor on the mROS problem, which admits sub-exponential attacks.¶
The Anchor's view of a session is the blinded challenge c alone. Because
gamma2 is uniform and nonzero, c is uniformly distributed and independent
of the message and of the resulting signature. The scheme is perfectly blind
[TESSZHU], so an Anchor cannot link an Endorsement to the session that
produced it, even with unbounded computation. This is what makes endorsement
grants and redemptions unlinkable as required by [ARCH], including against
an attacker with a quantum computer that records transcripts today.¶
Blindness is unconditional only if the blinding factors are. Challenge
derives them from seeds rather than sampling them, so the guarantee is
statistical rather than perfect: an Endorsement and a session are linkable
by an adversary that can find a seed consistent with both. Two properties of
Section 4.2 keep the loss negligible. Each scalar gets its own seed, so
a seed consistent with any given value exists with overwhelming probability
and finding one therefore separates nothing; and each seed is Ns + 16 bytes,
so each derived scalar is within about 2^-128 of uniform. Deriving several
scalars from one seed, or from a seed of Ns bytes, would break this: the
blinding factors of a session would then be jointly determined by fewer bits
than they contain, an exhaustive search over seeds would identify the one
session consistent with a given Endorsement, and unlinkability would hold
only against a bounded adversary. Implementations MUST NOT do either.¶
A Client that completes n issuance sessions under a given issuance context
cannot produce n+1 distinct valid Endorsements under that context,
regardless of how many sessions it has completed under other issuance
contexts [TESSZHU]. This is what allows a Moderator to conclude that an
accepted Endorsement corresponds to exactly one grant by a trusted Anchor.¶
Implementations MUST ensure that the session state produced by Commit is
never used more than once. This requirement is load-bearing, not defensive.
If an Anchor answers two distinct challenges c1 != c2 on one Commit state,
then from the two responses s1 = a + c1*y*skA and s2 = a + c2*y*skA, with
y revealed in both, anyone recovers the signing key as
skA = (s1 - s2) * ScalarInverse((c1 - c2) * y). The requirement extends to
process restarts, to replicas sharing a signing key, and to any retry or
replay of a ChallengeMessage: an Anchor MUST treat a session as closed the
moment it emits a response, and MUST answer a repeated session_id with a
SessionError rather than recomputing. Anchors are stateful for this reason,
and this state cannot be made stateless by sealing it into a cookie handed to
the Client: sealing preserves the secrecy of (a, y, t) but not their
single use, and single use is the property that matters here.¶
The issuance context enters both the commitment base and the challenge
transcript, and the redemption context enters the signed message, so an
Endorsement does not verify under any other pair of contexts. Neither context
is carried in the Endorsement; both are supplied by the verifier
(Section 5.5), so a Client cannot assert the pair its Endorsement is checked
against. A Client also cannot select the issuance context unilaterally: it is
never sent from the Client to the Anchor, and using a value other than the one
the Anchor committed under fails the opening check in Finalize.¶
A Client that reuses a nullifier across Endorsements links those Endorsements to each other at redemption and, depending on the Moderator's nullifier store, causes all but the first redemption to be rejected. Nullifiers MUST be freshly generated.¶
Challenge aborts when the challenge hashes to zero and Respond aborts on
a zero challenge. Both events occur with probability approximately 1/p for
honest parties, where p is the order of the group. A Client that observes
such an abort learns nothing and SHOULD start a fresh session.¶
The unlinkability arguments above are cryptographic; the anonymity set they
operate over is set by the contexts. Both contexts are visible at redemption,
the issuance context directly and the redemption context through the fact
that the Endorsement verifies under it, so each partitions Clients into the
set that shares its value. Whatever [ARCH] eventually specifies them to be
(Section 4.4), both values must therefore be coarse. Every Client
holding an Endorsement issued under a given issuance context MUST derive the
byte-identical ctx_iss, and every Client redeeming under a given
redemption context MUST derive the byte-identical ctx_red. A deployment
that refines either value, for instance by using a per-request timestamp
rather than a shared epoch, or a per-Client identifier rather than a value
shared by every Client redeeming in the same place, reduces the anonymity set
accordingly, in the limit to a single Client, and does so without violating
any cryptographic property of the construction. Implementations MUST NOT do
so.¶
The session_id of Section 5.6.2 is chosen by the Anchor and so is a value
the Anchor recognises. It is confined to the transport: it is not an input to
any algorithm of Section 5 and MUST NOT enter the challenge transcript. Were
it bound into the Endorsement, the Anchor could recognise its own identifier at
redemption and link the redemption to the issuance session.¶
The effective privacy a Client obtains also depends on deployment properties beyond this document, in particular the number of Clients an Anchor serves per epoch and the size of a Moderator's Anchor Set; see [ARCH].¶
This document has no IANA actions. The endorsement type for the scheme specified here is registered by [PROTOCOLS].¶
TODO. Test vectors for DeriveKeyPair, DeriveScalar, CreateContextBase,
Message, ComputeChallenge, the four issuance algorithms, and Verify, for
each ciphersuite in Section 7. Issuance is randomized, but every
algorithm is a deterministic function of the bytes it draws from random, so
a vector fixes one value per algorithm: the key seed, the rand of Commit,
and the rand of Challenge.¶
TODO acknowledge.¶