Skip to main content

Anonymous Tokens with Hidden Metadata
draft-yun-cfrg-athm-00

Document Type Active Internet-Draft (individual)
Authors Cathie Yun , Christopher A. Wood , Mariana Raykova , Samuel Schlesinger
Last updated 2025-10-20
RFC stream (None)
Intended RFC status (None)
Formats
Stream Stream state (No stream defined)
Consensus boilerplate Unknown
RFC Editor Note (None)
IESG IESG state I-D Exists
Telechat date (None)
Responsible AD (None)
Send notices to (None)
draft-yun-cfrg-athm-00
Crypto Forum                                                      C. Yun
Internet-Draft                                                C. A. Wood
Intended status: Informational                               Apple, Inc.
Expires: 23 April 2026                                        M. Raykova
                                                          S. Schlesinger
                                                                  Google
                                                         20 October 2025

                 Anonymous Tokens with Hidden Metadata
                         draft-yun-cfrg-athm-00

Abstract

   This document specifies the Anonymous Tokens with Hidden Metadata
   (ATHM) protocol, a protocol for constructing Privacy Pass like tokens
   with hidden metadata embedded within it unknown to the client.

About This Document

   This note is to be removed before publishing as an RFC.

   The latest revision of this draft can be found at
   https://cathieyun.github.io/draft-athm/draft-yun-cfrg-athm.html.
   Status information for this document may be found at
   https://datatracker.ietf.org/doc/draft-yun-cfrg-athm/.

   Discussion of this document takes place on the Crypto Forum Research
   Group mailing list (mailto:cfrg@ietf.org), which is archived at
   https://mailarchive.ietf.org/arch/browse/cfrg.  Subscribe at
   https://www.ietf.org/mailman/listinfo/cfrg/.

   Source for this draft and an issue tracker can be found at
   https://github.com/cathieyun/draft-athm.

Status of This Memo

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

Yun, et al.               Expires 23 April 2026                 [Page 1]
Internet-Draft                    ATHM                      October 2025

   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 23 April 2026.

Copyright Notice

   Copyright (c) 2025 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.

Table of Contents

   1.  Introduction  . . . . . . . . . . . . . . . . . . . . . . . .   3
   2.  Conventions and Definitions . . . . . . . . . . . . . . . . .   3
     2.1.  Notation and Terminology  . . . . . . . . . . . . . . . .   3
   3.  Preliminaries . . . . . . . . . . . . . . . . . . . . . . . .   4
     3.1.  Prime-Order Group . . . . . . . . . . . . . . . . . . . .   5
   4.  Ciphersuites  . . . . . . . . . . . . . . . . . . . . . . . .   6
     4.1.  ATHM(P-256) . . . . . . . . . . . . . . . . . . . . . . .   7
     4.2.  Random Scalar Generation  . . . . . . . . . . . . . . . .   8
       4.2.1.  Rejection Sampling  . . . . . . . . . . . . . . . . .   8
       4.2.2.  Random Number Generation Using Extra Random Bits  . .   9
   5.  Anonymous Token with Hidden Metadata Protocol . . . . . . . .   9
     5.1.  Key Generation and Context Setup  . . . . . . . . . . . .   9
       5.1.1.  Public Key Proof  . . . . . . . . . . . . . . . . . .  11
     5.2.  ATHM Protocol . . . . . . . . . . . . . . . . . . . . . .  12
     5.3.  TokenRequest  . . . . . . . . . . . . . . . . . . . . . .  14
     5.4.  TokenResponse . . . . . . . . . . . . . . . . . . . . . .  15
       5.4.1.  FinalizeToken . . . . . . . . . . . . . . . . . . . .  19
       5.4.2.  VerifyToken . . . . . . . . . . . . . . . . . . . . .  22
   6.  Security Considerations . . . . . . . . . . . . . . . . . . .  23
   7.  IANA Considerations . . . . . . . . . . . . . . . . . . . . .  24
   8.  References  . . . . . . . . . . . . . . . . . . . . . . . . .  24
     8.1.  Normative References  . . . . . . . . . . . . . . . . . .  24
     8.2.  Informative References  . . . . . . . . . . . . . . . . .  24
   Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . .  25
   Test Vectors  . . . . . . . . . . . . . . . . . . . . . . . . . .  25
     ATHM(P-256) . . . . . . . . . . . . . . . . . . . . . . . . . .  25
   Authors' Addresses  . . . . . . . . . . . . . . . . . . . . . . .  27

Yun, et al.               Expires 23 April 2026                 [Page 2]
Internet-Draft                    ATHM                      October 2025

1.  Introduction

   This document presents a construction for Anonymous Tokens with
   Hidden Metadata (ATHM).  ATHM is a cryptographic primitive that
   enables an issuer to issue an authentication token to a client in a
   way that the token carries no information about the client identity
   except a hidden metadata value the issuer sets during issuance.  The
   value of the hidden metadata comes from a fixed domain and the client
   can verify this property of the hidden metadata of the token it
   receives.  The metadata value remains hidden with respect to any
   party which does not have the secret key for the ATHM construction,
   including the client.  Only the issuer who has the secret for the
   construction can verify the token and read the hidden metadata value.

   Similarly to public metadata proposed in the context of existing
   anonymous tokens schemes, the hidden metadata enables the issuer to
   partition clients into a number of groups proportional to the domain
   of the hidden metadata.  During token redemption the issuer can
   identify which group the client belongs to based on the metadata
   value.  The difference from public metadata is that this value of
   hidden metadata is only readable by the issuer who has the ATHM
   secret key, while public metadata embedded in the token is visible to
   everyone.

   Anonymous tokens are used for client authentication and their meaning
   is to convey trust in the client.  In most cases the trust assigned
   to a client is determined based on the outcome of fraud and spam
   detection processes.  It is often crucial that the signals used to
   obtain the verdict and the verdict itself remain hidden from the
   adversary, who can adapt its behavior to avoid detection if it knows
   when and why its behavior has been identified as fraudulent.  ATHM
   can provide the means to issue authentication tokens that provide
   limited hidden fraud signals while preserving strong anonymity
   guarantees.

2.  Conventions and Definitions

2.1.  Notation and Terminology

   The following functions and notation are used throughout the
   document.

   *  For any object x, we write len(x) to denote its length in bytes.

   *  For two byte arrays x and y, write x || y to denote their
      concatenation.

Yun, et al.               Expires 23 April 2026                 [Page 3]
Internet-Draft                    ATHM                      October 2025

   *  I2OSP(x, xLen): Converts a non-negative integer x into a byte
      array of specified length xLen as described in [RFC8017].  Note
      that this function returns a byte array in big-endian byte order.

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

   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.

   String values such as "CredentialRequest", "CredentialResponse", and
   "Presentation" are ASCII string literals.

   The PrivateInput data type refers to inputs that are known only to
   the client in the protocol, whereas the PublicInput data type refers
   to inputs that are known to both client and server in the protocol.

   The following terms are used throughout this document.

   *  Client: Protocol initiator.  Creates an encrypted request, and
      uses the corresponding server encrypted issuance to make a
      presentation.

   *  Server: Computes an encrypted issuance for an encrypted 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
      issuance and presentation steps.

3.  Preliminaries

   The construction in this document has two primary dependencies:

   *  Group: A prime-order group implementing the API described below in
      Section 3.1.  See Section 4 for specific instances of groups.

   *  Hash: A cryptographic hash function whose output length is Nh
      bytes.

   Section 4 specifies ciphersuites as combinations of Group and Hash.

Yun, et al.               Expires 23 April 2026                 [Page 4]
Internet-Draft                    ATHM                      October 2025

3.1.  Prime-Order Group

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

   *  Generators(): Outputs the generator elements of the group,
      (generatorG, generatorH) generatorG = Group.generator generatorH =
      HashToGroup(SerializeElement(generatorG), "generatorH").  The
      group member functions GeneratorG() and GeneratorH() are shorthand
      for returning generatorG and generatorH, respectively.

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

Yun, et al.               Expires 23 April 2026                 [Page 5]
Internet-Draft                    ATHM                      October 2025

   *  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 4 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 4 for group-specific input
      validation steps.

   Section 4 contains details for the implementation of this interface
   for different prime-order groups instantiated over elliptic curves.
   In particular, for some choices of elliptic curves, e.g., those
   detailed in [RFC7748], which require accounting for cofactors,
   Section 4 describes required steps necessary to ensure the resulting
   group is of prime order.

4.  Ciphersuites

   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 instantiations of the following
   functionalities:

   *  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

Yun, et al.               Expires 23 April 2026                 [Page 6]
Internet-Draft                    ATHM                      October 2025

      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.

   *  Hash: A cryptographic hash function whose output length is Nh
      bytes long.

   This section includes an initial set of ciphersuites with supported
   groups and hash functions.  It also includes implementation details
   for each ciphersuite, focusing on input validation.

   For each ciphersuite, contextString is that which is computed in the
   Setup functions.  Applications should take caution in using
   ciphersuites targeting P-256 and ristretto255.

4.1.  ATHM(P-256)

   This ciphersuite uses P-256 [NISTCurves] for the Group.  The value of
   the ciphersuite identifier is "P256".

   *  Group: P-256 (secp256r1) [NISTCurves]

      -  Order(): Return 0xffffffff00000000ffffffffffffffffbce6faada7179
         e84f3b9cac2fc632551.

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

Yun, et al.               Expires 23 April 2026                 [Page 7]
Internet-Draft                    ATHM                      October 2025

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

4.2.  Random Scalar Generation

   Two popular algorithms for generating a random integer uniformly
   distributed in the range [0, G.Order() -1] are as follows:

4.2.1.  Rejection Sampling

   Generate a random byte array with Ns bytes, and attempt to map to a
   Scalar by calling DeserializeScalar in constant time.  If it
   succeeds, return the result.  If it fails, 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 - 2^b| is less than 2^(b/2), then RandomScalar can simply
   return a uniformly random integer of at most b bits.

Yun, et al.               Expires 23 April 2026                 [Page 8]
Internet-Draft                    ATHM                      October 2025

4.2.2.  Random Number Generation Using Extra Random 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.

5.  Anonymous Token with Hidden Metadata Protocol

   TODO(caw): writeme

5.1.  Key Generation and Context Setup

   In the offline phase, the server generates a public and private key
   using the KeyGen routine below.  The type PublicKeyProof and the
   function CreatePublicKeyProof are specified in Section 5.1.1.
   Further, they must agree on a string deploymentId and a number of
   buckets nBuckets.  The contextString as used in HashToGroup and
   HashToCurve is ATHMV1-<group-name>-<nBuckets>-<deploymentId>.  An
   example of contextString is ATHMV1-P256-4-example_deployment_id.

Yun, et al.               Expires 23 April 2026                 [Page 9]
Internet-Draft                    ATHM                      October 2025

   Input: None
   Output:
   - privateKey:
     - x: Scalar
     - y: Scalar
     - z: Scalar
     - r_x: Scalar
     - r_y: Scalar
   - publicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element
     - pi: PublicKeyProof

   Parameters
   - Group G

   def KeyGen():
     x = G.RandomScalar()
     y = G.RandomNonzeroScalar()
     z = G.RandomNonzeroScalar()
     r_x = G.RandomScalar()
     r_y = G.RandomScalar()

     Z = z * G.GeneratorG()
     C_x = (x * G.GeneratorG()) + (r_x * G.GeneratorH())
     C_y = (y * G.GeneratorG()) + (r_y * G.GeneratorH())
     pi = CreatePublicKeyProof(z, Z)

     return privateKey(x, y, z, r_x, r_y), publicKey(Z, C_x, C_y, pi)

   The output publicKey from this function is wire-encoded into
   publicKey = 3*Ne+2*Ns bytes as follows:

   struct {
     uint8 Z_enc[Ne];
     uint8 C_x_enc[Ne];
     uint8 C_y_enc[Ne];
     uint8 pi_enc[Nproof];
   }

   The Z_enc, C_x_enc, and C_y_enc fields are the serialized
   representations of publicKey.Z, publicKey.C_x, and publicKey.C_y,
   respectively.  The pi_enc field is the serialized PublicKeyProof, as
   defined below.

Yun, et al.               Expires 23 April 2026                [Page 10]
Internet-Draft                    ATHM                      October 2025

5.1.1.  Public Key Proof

   The server public key carries with it a zero-knowledge proof of
   knowledge of the corresponding private key.  Clients verify this
   proof before using the public key for token issuance.  The procedures
   for creating and verifying this proof, CreatePublicKeyProof and
   VerifyPublicKeyProof, are detailed below.

   Input:
   - z: Scalar
   - Z: Element

   Output:
   - pi: PublicKeyProof
     - e: Scalar
     - a_z: Scalar

   Parameters
   - Group G

   def CreatePublicKeyProof(z, Z):
     rho_z = G.RandomScalar()
     gamma_z = rho_z * G.GeneratorG()

     ser_genG = G.SerializeElement(G.GeneratorG())
     ser_Z = G.SerializeElement(Z)
     ser_gamma_z = G.SerializeElement(gamma_big_z)

     challenge_transcript =
       I2OSP(len(ser_genG), 2) + ser_genG +
       I2OSP(len(ser_Z), 2) + ser_Z +
       I2OSP(len(ser_gamma_z), 2) + ser_gamma_z

     e = G.HashToScalar(challenge_transcript, "KeyCommitments")
     a_z = rho_z - (e * z)

     return PublicKeyProof(e, a_z)

   The output of CreatePublicKeyProof is a value of type PublicKeyProof,
   which is wire-encoded as the concatenation of the e and a_z values,
   both serialized using SerializeScalar, yielding the following format:

   struct {
     uint8 e_enc[Ns];
     uint8 a_z_enc[Ns];
   } PublicKeyProof;

Yun, et al.               Expires 23 April 2026                [Page 11]
Internet-Draft                    ATHM                      October 2025

   The VerifyPublicKeyProof routine, which takes as input a server
   public key, is below.

   Input:
   - publicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element
     - pi: PublicKeyProof

   Output:
   - verifiedPublicKey if valid, False otherwise

   def VerifyPublicKeyProof(publicKey):
     pi = publicKey.pi
     gamma_z = (pi.e * publicKey.Z) +
       (pi.a_z * G.GeneratorG())

     ser_genG = G.SerializeElement(G.GeneratorG())
     ser_Z = G.SerializeElement(Z)
     ser_gamma_z = G.SerializeElement(gamma_big_z)

     challenge_transcript =
       I2OSP(len(ser_genG), 2) + ser_genG +
       I2OSP(len(ser_Z), 2) + ser_Z +
       I2OSP(len(ser_gamma_z), 2) + ser_gamma_z

     e_verify = G.HashToScalar(challenge_transcript, "KeyCommitments")
     if e_verify == e:
       return verifiedPublicKey(Z, C_x, C_y)
     return False

   The output verifiedPublicKey from this function is wire-encoded into
   verifiedPublicKey = 3*Ne bytes as follows:

   struct {
     uint8 Z_enc[Ne];
     uint8 C_x_enc[Ne];
     uint8 C_y_enc[Ne];
   }

5.2.  ATHM Protocol

   The ATHM Protocol is a two-party protocol between a client and server
   where they interact to compute

   token = F(privateKey, publicKey, hiddenMetadata)

Yun, et al.               Expires 23 April 2026                [Page 12]
Internet-Draft                    ATHM                      October 2025

   where privateKey and publicKey contains the server's private and
   public keys, respectively (and generated as described in
   Section 5.1), and hiddenMetadata is an application-specific value
   known only to the server.

   The protocol begins with the client making a token request using the
   verified server public key.

   verifiedPublicKey = VerifyPublicKeyProof(publicKey, pi)
   (context, request) = TokenRequest(verifiedPublicKey)

   The client then sends request to the server.  If the request is well-
   formed, the server computes a token response with the server private
   keys and hidden metadata.  The response includes a proof that the
   token response is valid with respect to the server keys, and a
   maximum number of buckets for the hidden metadata.

response = CredentialResponse(privateKey, publicKey, request, hiddenMetadata, nBuckets)

   The server sends the response to the client.  The client processes
   the response by verifying the response proof.  If the proof verifies
   correctly, the client computes a token from its context and the
   server response:

token = FinalizeToken(context, verifiedPublicKey, request, response, nBuckets)

   When the client presents the token to the server for redemption, the
   server verifies it using its private keys, as follows:

   hiddenMetadata = VerifyToken(privateKey, token, nBuckets)

   This process fails if the token is invalid, and otherwise returns the
   hidden metadata associated with the token during issuance.

   Shown graphically, the protocol runs as follows:

Yun, et al.               Expires 23 April 2026                [Page 13]
Internet-Draft                    ATHM                      October 2025

   Client(publicKey)                      Server(privateKey, publicKey, hiddenMetadata)
         ---                                                  ---
  verifiedPublicKey = VerifyPublicKeyProof(publicKey, pi)
  (context, request) = TokenRequest(verifiedPublicKey)

                                request
                            --------------->

                         response = TokenResponse(privateKey, publicKey, request, hiddenMetadata, nBuckets)

                                response
                            <---------------

  token = FinalizeToken(context, verifiedPublicKey, request, response, nBuckets)

     ....

                                  token
                            ------------------->

                    hiddenMetadata = VerifyToken(privateKey, token, nBuckets)

   In the remainder of this section, we specify the TokenRequest,
   TokenResponse, FinalizeToken, and VerifyToken functions that are used
   in this protocol.

5.3.  TokenRequest

   The TokenRequest function is defined below.

Yun, et al.               Expires 23 April 2026                [Page 14]
Internet-Draft                    ATHM                      October 2025

   Inputs:
   - verifiedPublicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element

   Outputs:
   - context:
     - r: Scalar
     - tc: Scalar
   - request:
     - T: Element

   Parameters:
   - G: Group

   def TokenRequest(client):
     r = G.RandomScalar()
     tc = G.RandomScalar()
     T = (r * G.GeneratorG()) + (tc * verifiedPublicKey.Z)
     return context(r, tc), request(T)

   The output request from this function is wire-encoded into
   Nrequest=Ne bytes as follows:

   struct {
     uint8 T_enc[Ne];
   }

   The T_enc field is the serialized representation of request.T.

5.4.  TokenResponse

   The TokenResponse function is defined below.

Yun, et al.               Expires 23 April 2026                [Page 15]
Internet-Draft                    ATHM                      October 2025

   Inputs:
   - privateKey:
     - x: Scalar
     - y: Scalar
     - z: Scalar
     - r_x: Scalar
     - r_y: Scalar
   - publicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element
     - pi: PublicKeyProof
   - request:
     - T: Element
   - hiddenMetadata: Integer
   - nBuckets: Integer

   Outputs:
   - response:
     - U: Element
     - V: Element
     - ts: Scalar
     - pi: IssuanceProof

   Parameters:
   - G: Group

   def TokenResponse(privateKey, publicKey, request, hiddenMetadata, nBuckets):
     ts = G.RandomScalar()
     d = G.RandomNonzeroScalar()

     U = d * G.GeneratorG()
     X = privateKey.x * GeneratorG()
     Y = privateKey.y * GeneratorG()
     V = d * (X + (hiddenMetadata * Y) + (ts * publicKey.Z) + request.T)

     pi = CreateIssuanceProof(privateKey, publicKey, hiddenMetadata, nBuckets, d, U, V, ts, T)

     return response(U, V, ts, pi)

   The output response from this function is wire-encoded into
   Nresponse=Ne+Ne+Ns+Nproof bytes as follows:

Yun, et al.               Expires 23 April 2026                [Page 16]
Internet-Draft                    ATHM                      October 2025

   struct {
     uint8 U_enc[Ne];
     uint8 V_enc[Ne];
     uint8 ts_enc[Ns];
     uint8 pi_enc[Nproof];
   }

   The U_enc, V_enc, and ts_enc fields are the serialized
   representations of response.U, response.V, and response.ts,
   respectively.  The pi_enc field is the serialized IssuanceProof, as
   defined below.

   The CreateIssuanceProof function is defined below.

   Inputs:
   - privateKey:
     - x: Scalar
     - y: Scalar
     - z: Scalar
     - r_x: Scalar
     - r_y: Scalar
   - publicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element
     - pi: PublicKeyProof
   - hiddenMetadata: Integer
   - nBuckets: Integer
   - d: Scalar
   - U: Element
   - V: Element
   - ts: Scalar
   - T: Element

   Output:
   - pi: IssuanceProof
     - C: Element
     - e: [Scalar]
     - a: [Scalar]
     - a_d: Scalar
     - a_rho: Scalar
     - a_w: Scalar

   Parameters:
   - G: Group

   def CreateIssuanceProof(privateKey, publicKey, hiddenMetadata, nBuckets, d, U, V, ts, T):
     e_vec = [G.RandomScalar() for i in range(nBuckets)]

Yun, et al.               Expires 23 April 2026                [Page 17]
Internet-Draft                    ATHM                      October 2025

     e_vec[hiddenMetadata] = Scalar(0) # zero for now - will be calculated and set later.
     a_vec = [G.RandomScalar() for i in range(nBuckets)]
     a_vec[hiddenMetadata] = Scalar(0) # zero for now - will be calculated and set later.
     r_mu = G.RandomScalar()
     r_d = G.RandomScalar()
     r_rho = G.RandomScalar()
     r_w = G.RandomScalar()
     mu = G.RandomScalar()

     C = hiddenMetadata * publicKey.C_y + mu * G.GeneratorH()
     C_vec = []
     for i in range(nBuckets):
       if i == hiddenMetadata:
         # This is the actual C[i] value for the "correct" slot (i == hiddenMetadata)
         C_vec.append(r_mu * G.GeneratorH())
       else:
         # This is the simulated C[i] value for the "incorrect" slot (i != hiddenMetadata)
         C_vec.append(a_vec[i] * G.GeneratorH() - e_vec[i] * (C - (i * pk.C_y)))
     C_d = r_d * U
     C_rho = (r_d * V) + (r_rho * G.GeneratorH())
     C_w = (r_d * V) + (r_w * G.GeneratorG())

     ser_genG = G.SerializeElement(G.GeneratorG())
     ser_genH = G.SerializeElement(G.GeneratorH())
     ser_C_x = G.SerializeElement(publicKey.C_x)
     ser_C_y = G.SerializeElement(publicKey.C_y)
     ser_Z = G.SerializeElement(publicKey.Z)
     ser_U = G.SerializeElement(U)
     ser_V = G.SerializeElement(V)
     ser_ts = G.SerializeScalar(ts)
     ser_T = G.SerializeElement(T)
     ser_C = G.SerializeElement(C)
     ser_C_d = G.SerializeElement(C_d)
     ser_C_rho = G.SerializeElement(C_rho)
     ser_C_w = G.SerializeElement(C_w)

     challenge_transcript = \
       I2OSP(len(ser_genG), 2) + ser_genG + \
       I2OSP(len(ser_genH), 2) + ser_genH + \
       I2OSP(len(ser_C_x), 2) + ser_C_x + \
       I2OSP(len(ser_C_y), 2) + ser_C_y + \
       I2OSP(len(ser_Z), 2) + ser_Z + \
       I2OSP(len(ser_U), 2) + ser_U + \
       I2OSP(len(ser_V), 2) + ser_V + \
       I2OSP(len(ser_ts), 2) + ser_ts + \
       I2OSP(len(ser_T), 2) + ser_T + \
       I2OSP(len(ser_C), 2) + ser_C
     for i in range(nBuckets):

Yun, et al.               Expires 23 April 2026                [Page 18]
Internet-Draft                    ATHM                      October 2025

       ser_C_i = G.SerializeElement(C_vec[i])
       challenge_transcript.append(I2OSP(len(ser_C_i), 2) + ser_C_i)
     challenge_transcript.append( \
       I2OSP(len(ser_C_d), 2) + ser_C_d + \
       I2OSP(len(ser_C_rho), 2) + ser_C_rho + \
       I2OSP(len(ser_C_w), 2) + ser_C_w
     )

     e = G.HashToScalar(challenge_transcript, "TokenResponseProof")
     # Set the correct e_vec[hiddenMetadata] value.
     e_vec[hiddenMetadata] = e - sum(e_vec)

     d_inv = G.ScalarInverse(d)
     rho = -(privateKey.r_x + (hiddenMetadata * privateKey.r_y) + mu)
     w = privateKey.x + (hiddenMetadata * privateKey.y) + (ts * privateKey.z)

     # Set the correct a_vec[hiddenMetadata] value.
     a_vec[hiddenMetadata] = r_mu + (e_vec[hiddenMetadata] * mu)
     a_d = r_d - (e * d_inv)
     a_rho = r_rho + (e * rho)
     a_w = r_w + (e * w)

     return IssuanceProof(C, e_vec, a_vec, a_d, a_rho, a_w)

   The output pi from this function is wire-encoded into Nproof =
   Ne+(3+2*nBuckets)*Ns bytes as follows:

   struct {
     uint8 C_enc[Ne];
     [uint8] e_0_enc[Ns]...e_nBuckets_enc[Ns];
     [uint8] a_0_enc[Ns]...a_nBuckets_enc[Ns];
     uint8 a_d_enc[Ns];
     uint8 a_rho_enc[Ns];
     uint8 a_w_enc[Ns];
   }

   The fields in this structure are the serialized representations of
   the contents of pi, e.g., C_enc is the serialized representation of
   pi.C and e_0_enc is the serialized representation of e[0].

5.4.1.  FinalizeToken

   The FinalizeToken function is defined below.  Internally, the client
   verifies the token response proof.  FinalizeToken fails if this proof
   is invalid.

Yun, et al.               Expires 23 April 2026                [Page 19]
Internet-Draft                    ATHM                      October 2025

Inputs:
- context:
  - r: Scalar
  - tc: Scalar
- verifiedPublicKey:
  - Z: Element
  - C_x: Element
  - C_y: Element
- request:
  - T: Element
- response:
  - U: Element
  - V: Element
  - ts: Scalar
  - pi: IssuanceProof
- nBuckets: Integer

Outputs:
- token:
  - t: Scalar
  - P: Element
  - Q: Element

Parameters:
- G: Group

Exceptions:
- VerifyError, raised when response proof verification fails

def FinalizeToken(context, verifiedPublicKey, request, response, nBuckets):
  if VerifyIssuanceProof(verifiedPublicKey, request, response, nBuckets) == false:
    raise VerifyError

  c = G.RandomNonzeroScalar()
  P = c * response.U
  Q = c * (response.V - (context.r * response.U))
  t = context.tc + response.ts

  return token(t, P, Q)

   The resulting token can be serialized as the concatenation of t, P,
   and Q serialized using their respective serialization functions,
   yielding the following struct:

Yun, et al.               Expires 23 April 2026                [Page 20]
Internet-Draft                    ATHM                      October 2025

   struct {
     uint8 t_enc[Ns];
     uint8 P_enc[Ne];
     uint8 Q_enc[Ne];
   }

   The VerifyIssuanceProof function is defined below.

   Inputs:
   - verifiedPublicKey:
     - Z: Element
     - C_x: Element
     - C_y: Element
   - T: Element
   - response:
     - U: Element
     - V: Element
     - ts: Scalar
     - pi: IssuanceProof
   - nBuckets: Integer

   Output:
   - True if valid, false otherwise

   Parameters:
   - G: Group

   def VerifyIssuanceProof(verifiedPublicKey, T, response, nBuckets):
     pi = response.pi

     C_vec = []
     for i in range(nBuckets):
       C_i = pi.a_vec[i] * G.GeneratorH - (pi.e_vec[i] * (pi.C - (i * verifiedPublicKey.C_y))

     e = sum(pi.e_vec)

     C_d = (response.pi.a_d * response.U) + (e * G.GeneratorG())

     C_rho = (response.pi.a_d * response.V)
         + (response.pi.a_rho * G.GeneratorH())
         + (e * (verifiedPublicKey.C_x + response.pi.C + (response.ts * verifiedPublicKey.Z) + T))

     C_w = (response.pi.a_d * response.V)
         + (response.pi.a_w * G.GeneratorG())
         + (e * T)

     ser_genG = G.SerializeElement(G.GeneratorG())
     ser_genH = G.SerializeElement(G.GeneratorH())

Yun, et al.               Expires 23 April 2026                [Page 21]
Internet-Draft                    ATHM                      October 2025

     ser_C_x = G.SerializeElement(verifiedPublicKey.C_x)
     ser_C_y = G.SerializeElement(verifiedPublicKey.C_y)
     ser_Z = G.SerializeElement(verifiedPublicKey.Z)
     ser_U = G.SerializeElement(response.U)
     ser_V = G.SerializeElement(response.V)
     ser_ts = G.SerializeScalar(response.ts)
     ser_T = G.SerializeElement(T)
     ser_C = G.SerializeElement(pi.C)
     ser_C_d = G.SerializeElement(C_d)
     ser_C_rho = G.SerializeElement(C_rho)
     ser_C_w = G.SerializeElement(C_w)

     challenge_transcript = \
       I2OSP(len(ser_genG), 2) + ser_genG + \
       I2OSP(len(ser_genH), 2) + ser_genH + \
       I2OSP(len(ser_C_x), 2) + ser_C_x + \
       I2OSP(len(ser_C_y), 2) + ser_C_y + \
       I2OSP(len(ser_Z), 2) + ser_Z + \
       I2OSP(len(ser_U), 2) + ser_U + \
       I2OSP(len(ser_V), 2) + ser_V + \
       I2OSP(len(ser_ts), 2) + ser_ts + \
       I2OSP(len(ser_T), 2) + ser_T + \
       I2OSP(len(ser_C), 2) + ser_C + \
     for i in range(nBuckets):
       ser_C_i = G.SerializeElement(pi.C_vec[i])
       challenge_transcript.append(I2OSP(len(ser_C_i), 2) + ser_C_i)
     challenge_transcript.append( \
       I2OSP(len(ser_C_d), 2) + ser_C_d + \
       I2OSP(len(ser_C_rho), 2) + ser_C_rho + \
       I2OSP(len(ser_C_w), 2) + ser_C_w
     )

     e_verify = G.HashToScalar(challenge_transcript, "TokenResponseProof")
     return e == e_verify

5.4.2.  VerifyToken

   The VerifyToken token is defined below.

Yun, et al.               Expires 23 April 2026                [Page 22]
Internet-Draft                    ATHM                      October 2025

Inputs:
- privateKey:
  - x: Scalar
  - y: Scalar
  - z: Scalar
  - r_x: Scalar
  - r_y: Scalar
- token:
  - t: Scalar
  - P: Element
  - Q: Element
- nBuckets: Integer

Outputs:
- hiddenMetadata: Integer

Parameters:
- G: Group

Exceptions:
- VerifyError, raised when token verification fails

def VerifyToken(privateKey, token, nBuckets):
  if (token.P == token.P + token.P) || (token.Q == token.Q + token.Q):
    raise VerifyError # P and Q should not be zero

  iMatch = -1
  for i in range(nBuckets):
    Q_i = (privateKey.x + token.t * privateKey.z + i * privateKey.y) * token.P
    if token.Q == Q_i:
      if iMatch != -1:
        raise VerifyError # Multiple metadata values match
      iMatch = i

  if iMatch != -1:
    return iMatch
  else:
    raise VerifyError # No metadata values match

6.  Security Considerations

   The work of [CDV22] proves the following properties for the ATHM
   construction presented here: unforgeability, unlinkability and
   privacy of the metadata.  Unforgeability guarantees that only an
   issuer with a valid secret key can generate new tokens.
   Unlinkability states that tokens with the same metadata are
   indistinguishable to the redeemer, i.e. the redeemer does not have
   any advatage in guessing which issuance session (among those assigned

Yun, et al.               Expires 23 April 2026                [Page 23]
Internet-Draft                    ATHM                      October 2025

   the same metadata) a paricular redeemed token comes from.  Finally,
   privacy of the metadata guarantees that any party who does not know
   the secret verification key cannot learn any information about the
   hidden metadata of a token.

   The only place where the construction presented in this document
   differs from the ATHM construcion presented in [CDV22] is the fact
   that we generalize that construction to more than one bit for the
   hidden metadata.  This requires that the ZK proof provided during
   issuance to the client proves a new bound on the number of embedded
   bits in the token.  The proofs of unfogeability and the privacy of
   the hidden metadata bits do not depend on the range of the metadata.
   The unlinkability proof accounts for the fact that the issuer can
   partition the clients into a larger than two number of groups, since
   unlinkability only holds among tokens with the same metadata.

7.  IANA Considerations

   This document has no IANA actions.

8.  References

8.1.  Normative References

   [I-D.irtf-cfrg-hash-to-curve]
              Faz-Hernandez, A. F., Scott, S., Sullivan, N., Wahby, R.
              S., and C. A. Wood, "Hashing to Elliptic Curves", Work in
              Progress, Internet-Draft, draft-irtf-cfrg-hash-to-curve-
              16, 15 June 2022, <https://datatracker.ietf.org/doc/html/
              draft-irtf-cfrg-hash-to-curve-16>.

   [KEYAGREEMENT]
              Barker, E., Chen, L., Roginsky, A., Vassilev, A., and R.
              Davis, "Recommendation for pair-wise key-establishment
              schemes using discrete logarithm cryptography", National
              Institute of Standards and Technology,
              DOI 10.6028/nist.sp.800-56ar3, April 2018,
              <https://doi.org/10.6028/nist.sp.800-56ar3>.

   [RFC8017]  Moriarty, K., Ed., Kaliski, B., Jonsson, J., and A. Rusch,
              "PKCS #1: RSA Cryptography Specifications Version 2.2",
              RFC 8017, DOI 10.17487/RFC8017, November 2016,
              <https://www.rfc-editor.org/rfc/rfc8017>.

8.2.  Informative References

   [CDV22]    "Anonymous Tokens with Stronger Metadata Bit Hiding from
              Algebraic MACs", n.d., <https://eprint.iacr.org/2013/516>.

Yun, et al.               Expires 23 April 2026                [Page 24]
Internet-Draft                    ATHM                      October 2025

   [NISTCurves]
              "Digital signature standard (DSS)", National Institute of
              Standards and Technology (U.S.),
              DOI 10.6028/nist.fips.186-4, 2013,
              <https://doi.org/10.6028/nist.fips.186-4>.

   [RFC7748]  Langley, A., Hamburg, M., and S. Turner, "Elliptic Curves
              for Security", RFC 7748, DOI 10.17487/RFC7748, January
              2016, <https://www.rfc-editor.org/rfc/rfc7748>.

   [SEC1]     Standards for Efficient Cryptography Group (SECG), "SEC 1:
              Elliptic Curve Cryptography",
              <https://www.secg.org/sec1-v2.pdf>.

Acknowledgments

   Thanks to Melissa Chase for discussion about the ATHM paper.  Thanks
   also to Tommy Pauly, Phillipp Schoppmann and Ghous Amjad for
   collaboration on the ATHM specifications.

Test Vectors

   This section contains test vectors for the ATHM ciphersuites
   specified in this document.  The test vectors were generated from the
   reference implementation located at https://github.com/google/
   anonymous-tokens/pull/45/
   commits/8701431d31c2f49774526fac23bf7a2d24864314.  All byte strings,
   except deployment_id, are encoded in hexadecimal.

ATHM(P-256)

   [
     {
       "procedure": "params",
       "args": {},
       "output": {
         "deployment_id": "test_vector_deployment_id",
         "generator_g": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
         "generator_h": "02361fc6831d3796a82612dffb231ec67253b2f69dbb124c9a0f9917b4e3180d03",
         "n_buckets": "4"
       }
     },
     {
       "procedure": "key_gen",
       "args": {
         "rng_seed": "0101010101010101010101010101010101010101010101010101010101010101"
       },
       "output": {

Yun, et al.               Expires 23 April 2026                [Page 25]
Internet-Draft                    ATHM                      October 2025

         "key_id": "027defbe3a76d47f76e8e1296ddbadf8faeb91852a5964d7986ad974441dfc1c",
         "private_key": "023f37203a2476c42566a61cc55c3ca875dbb4cc41c0deb789f8e7bf881836381ecc3686b60ee3b84b6c7d321d70d5c06e9dac63a4d0a79d731b17c0d04d030d01274dd1ee5216c204fb698daea45b52e98b6f0fdd046dcc3a86bb079e36f024147e4b875d59a9ef432b8e45b04a98c4b19dc8c7475f5dce4259b4ca2dd67282b478b8702c1d2569fe52e5d7dbadec6223cd10fd4b504dabac7fff23a37363d1",
         "public_key": "032b80024ee818d709196780a84affe08789f571529fccc1acce07eea2df2fc65f035f6a6eb84bea9c1361119462bda863c47c3b43e53e9e5e7a8796ba1b924d093e03b7f16df0ad82cbd8bd6a04ed427107adf5a3d4ba840e9b341a3badc1d9b7fe19",
         "public_key_proof": "28ccf67d4ad339a45a8435b660160fef113a157221ce6f4a5c6f584a58d4509152852fcc950dd7495211dbefd34b733a730e5a2085e04f847e3c1af4355c59fe"
       }
     },
     {
       "procedure": "token_request",
       "args": {
         "public_key": "032b80024ee818d709196780a84affe08789f571529fccc1acce07eea2df2fc65f035f6a6eb84bea9c1361119462bda863c47c3b43e53e9e5e7a8796ba1b924d093e03b7f16df0ad82cbd8bd6a04ed427107adf5a3d4ba840e9b341a3badc1d9b7fe19",
         "public_key_proof": "28ccf67d4ad339a45a8435b660160fef113a157221ce6f4a5c6f584a58d4509152852fcc950dd7495211dbefd34b733a730e5a2085e04f847e3c1af4355c59fe",
         "rng_seed": "0202020202020202020202020202020202020202020202020202020202020202"
       },
       "output": {
         "token_context": "f6a12ca8ffc30a66ca140ccc7276336115819361186d3f535dd99f8eaaca8fce7f82dd63f4f75c33da444b72372be3aa43c0027a076bf9675eb7932695d127a4",
         "token_request": "030a1b41e492728f4f59d368761e1ff568536ad2987fd5be6017c5043ed25b67ea"
       }
     },
     {
       "procedure": "token_response",
       "args": {
         "hidden_metadata": "3",
         "private_key": "023f37203a2476c42566a61cc55c3ca875dbb4cc41c0deb789f8e7bf881836381ecc3686b60ee3b84b6c7d321d70d5c06e9dac63a4d0a79d731b17c0d04d030d01274dd1ee5216c204fb698daea45b52e98b6f0fdd046dcc3a86bb079e36f024147e4b875d59a9ef432b8e45b04a98c4b19dc8c7475f5dce4259b4ca2dd67282b478b8702c1d2569fe52e5d7dbadec6223cd10fd4b504dabac7fff23a37363d1",
         "public_key": "032b80024ee818d709196780a84affe08789f571529fccc1acce07eea2df2fc65f035f6a6eb84bea9c1361119462bda863c47c3b43e53e9e5e7a8796ba1b924d093e03b7f16df0ad82cbd8bd6a04ed427107adf5a3d4ba840e9b341a3badc1d9b7fe19",
         "rng_seed": "0303030303030303030303030303030303030303030303030303030303030303",
         "token_request": "030a1b41e492728f4f59d368761e1ff568536ad2987fd5be6017c5043ed25b67ea"
       },
       "output": {
         "token_response": "0245db306e323313840a20233ad432aae1be784f96aa940b628d56914249ce4b8c0250ccea1818d4ecf3026ebca1845fbff1cd0089868bbfdecac1c77b9ef2b00480385553aa23a24b14d8bbc2dff606277f444e049797ae7e0404e3a9ba0ecef2fb02d84ab8d7b059374064295a5b2c084c1814a4a33635769a6a196cb99c33122b6affb9e4ee66f5a203f5a31575fd70251b1cd922bcfaa87fc7c071df4412854ba49b424c1a1cded001b3ff8f0bc5306fa9950d236ce7075d9f702c3f0cd5546f8900a60c8dfef9f6633994aa4ed8b1ebd9111bcf05c62b39e9f7426edc7aab3fe679ff5c180ef849b41bd7033353818dd3e442431a1cb2c16992bdadef048a3673a43052fa4d37c9836e498619b9fc1584381292d0b1ec6f952fc6bab8035f6b30478648a01804edaba38b5cf910a932567c48e78b724d57400edd6f98cd565835bb1f733e33dc6335d9283d288ada574131ba288318476415651c571eee8430a75cae55cb231a35b67334ab9c30110d0bc6ce1d3654d4c44bbef74c3d572d432c7b4d06f693bd73505a93a56ab5190b18484c9e7231037fddfa45e1bcbb488def7d54110bbbe0a2a4799a6a8ccff13b1b347fe6867132b1dec0901f7bad11d7c2f8446cffc0f19d60db17c0db5770fdc83f70cd691de8b68289efafac5b098f63"
       }
     },
     {
       "procedure": "finalize_token",
       "args": {
         "public_key": "032b80024ee818d709196780a84affe08789f571529fccc1acce07eea2df2fc65f035f6a6eb84bea9c1361119462bda863c47c3b43e53e9e5e7a8796ba1b924d093e03b7f16df0ad82cbd8bd6a04ed427107adf5a3d4ba840e9b341a3badc1d9b7fe19",
         "rng_seed": "0404040404040404040404040404040404040404040404040404040404040404",
         "token_context": "f6a12ca8ffc30a66ca140ccc7276336115819361186d3f535dd99f8eaaca8fce7f82dd63f4f75c33da444b72372be3aa43c0027a076bf9675eb7932695d127a4",
         "token_request": "030a1b41e492728f4f59d368761e1ff568536ad2987fd5be6017c5043ed25b67ea",
         "token_response": "0245db306e323313840a20233ad432aae1be784f96aa940b628d56914249ce4b8c0250ccea1818d4ecf3026ebca1845fbff1cd0089868bbfdecac1c77b9ef2b00480385553aa23a24b14d8bbc2dff606277f444e049797ae7e0404e3a9ba0ecef2fb02d84ab8d7b059374064295a5b2c084c1814a4a33635769a6a196cb99c33122b6affb9e4ee66f5a203f5a31575fd70251b1cd922bcfaa87fc7c071df4412854ba49b424c1a1cded001b3ff8f0bc5306fa9950d236ce7075d9f702c3f0cd5546f8900a60c8dfef9f6633994aa4ed8b1ebd9111bcf05c62b39e9f7426edc7aab3fe679ff5c180ef849b41bd7033353818dd3e442431a1cb2c16992bdadef048a3673a43052fa4d37c9836e498619b9fc1584381292d0b1ec6f952fc6bab8035f6b30478648a01804edaba38b5cf910a932567c48e78b724d57400edd6f98cd565835bb1f733e33dc6335d9283d288ada574131ba288318476415651c571eee8430a75cae55cb231a35b67334ab9c30110d0bc6ce1d3654d4c44bbef74c3d572d432c7b4d06f693bd73505a93a56ab5190b18484c9e7231037fddfa45e1bcbb488def7d54110bbbe0a2a4799a6a8ccff13b1b347fe6867132b1dec0901f7bad11d7c2f8446cffc0f19d60db17c0db5770fdc83f70cd691de8b68289efafac5b098f63"
       },
       "output": {
         "token": "b7d8310e1899a748b3000e522d320b29880e07119f1a776b639b3ce0a4a01a9f02123d0c125de3d122577b335a8f6616d735e9400b60dcde57eff056e9cbbd2b3c03a062c5b41507e1fe089d0c3b4132d84ece1d4a9dfc0bf4f0588d660f25bdf6cf"
       }
     },
     {
       "procedure": "verify_token",
       "args": {
         "private_key": "023f37203a2476c42566a61cc55c3ca875dbb4cc41c0deb789f8e7bf881836381ecc3686b60ee3b84b6c7d321d70d5c06e9dac63a4d0a79d731b17c0d04d030d01274dd1ee5216c204fb698daea45b52e98b6f0fdd046dcc3a86bb079e36f024147e4b875d59a9ef432b8e45b04a98c4b19dc8c7475f5dce4259b4ca2dd67282b478b8702c1d2569fe52e5d7dbadec6223cd10fd4b504dabac7fff23a37363d1",

Yun, et al.               Expires 23 April 2026                [Page 26]
Internet-Draft                    ATHM                      October 2025

         "token": "b7d8310e1899a748b3000e522d320b29880e07119f1a776b639b3ce0a4a01a9f02123d0c125de3d122577b335a8f6616d735e9400b60dcde57eff056e9cbbd2b3c03a062c5b41507e1fe089d0c3b4132d84ece1d4a9dfc0bf4f0588d660f25bdf6cf"
       },
       "output": {
         "hidden_metadata": "3"
       }
     }
   ]

Authors' Addresses

   Cathie Yun
   Apple, Inc.
   Email: cathieyun@gmail.com

   Christopher A. Wood
   Apple, Inc.
   Email: caw@heapingbits.net

   Mariana Raykova
   Google
   Email: marianar@google.com

   Samuel Schlesinger
   Google
   Email: sgschlesinger@gmail.com

Yun, et al.               Expires 23 April 2026                [Page 27]