Network Working Group S. Schlesinger Internet-Draft J. Katz Intended status: Informational Google Expires: 25 August 2026 21 February 2026 Anonymous Credit Tokens draft-schlesinger-cfrg-act-latest Abstract This document specifies Anonymous Credit Tokens (ACT), a privacy- preserving authentication protocol that enables numerical credit systems without tracking individual clients. Based on keyed- verification anonymous credentials and privately verifiable BBS-style signatures, the protocol allows issuers to grant tokens containing credits that clients can later spend anonymously with that issuer. The protocol's key features include: (1) unlinkable transactions - the issuer cannot correlate credit issuance with spending, or link multiple spends by the same client, (2) partial spending - clients can spend a portion of their credits and receive anonymous change, and (3) double-spend prevention through cryptographic nullifiers that preserve privacy while ensuring each token is used only once. Anonymous Credit Tokens are designed for modern web services requiring rate limiting, usage-based billing, or resource allocation while respecting user privacy. Example applications include rate limiting and API credits. This document is a product of the Crypto Forum Research Group (CFRG) in the IRTF. 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://SamuelSchlesinger.github.io/draft-act/draft-schlesinger-cfrg- act.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-schlesinger-cfrg-act/. 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/SamuelSchlesinger/draft-act. 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/. Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." This Internet-Draft will expire on 25 August 2026. Copyright Notice 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. Table of Contents 1. Introduction 1.1. Key Properties 1.2. Use Cases 1.3. Protocol Overview 1.4. Design Goals 1.5. Relation to Existing Work 2. Conventions and Definitions 2.1. Notation 2.2. Data Types 2.3. Ciphersuites 2.3.1. ACT-Ristretto255-BLAKE3 2.3.2. ACT-P256-BLAKE3 2.4. Cryptographic Parameters 3. Protocol Specification 3.1. System Parameters 3.2. Key Generation 3.3. Token Issuance 3.3.1. Client: Issuance Request 3.3.2. Issuer: Issuance Response 3.3.3. Client: Token Verification 3.4. Token Spending 3.4.1. Client: Spend Proof Generation 3.4.2. Issuer: Spend Verification and Refund 3.4.3. Refund Issuance 3.4.4. Client: Refund Token Construction 3.4.5. Spend Proof Verification 3.5. Cryptographic Primitives 3.5.1. Protocol Version 3.5.2. Hash Function and Fiat-Shamir Transform 3.5.3. Encoding Functions 3.5.4. Binary Decomposition 3.5.5. Scalar Conversion 4. Protocol Messages and Wire Format 4.1. Message Encoding 4.1.1. Issuance Request Message 4.1.2. Issuance Response Message 4.1.3. Spend Proof Message 4.1.4. Refund Message 4.2. Error Responses 4.3. Key Serialization 4.3.1. Public Key 4.3.2. Private Key 4.4. Client State Serialization 4.4.1. Pre-Issuance State 4.4.2. Credit Token 4.4.3. Pre-Refund State 4.5. Protocol Flow 4.5.1. Example Usage Scenario 5. Implementation Considerations 5.1. Nullifier Management 5.2. Constant-Time Operations 5.3. Randomness Generation 5.3.1. RNG Requirements 5.3.2. Nonce Generation 5.4. Point Validation 5.5. Error Handling 5.5.1. Error Codes 5.6. Parameter Selection 5.6.1. Performance Characteristics 6. Security Considerations 6.1. Security Model and Definitions 6.1.1. Threat Model 6.1.2. Security Properties 6.2. Cryptographic Assumptions 6.3. Privacy Properties 6.4. Implementation Vulnerabilities and Mitigations 6.4.1. Critical Security Requirements 6.5. Known Attack Scenarios 6.5.1. 1. Parallel Spend Attack 6.5.2. 2. Balance Inflation Attack 6.5.3. 3. Token Linking Attack 6.6. Protocol Composition and State Management 6.6.1. State Management Requirements 6.6.2. Session Management 6.6.3. Version Negotiation 6.7. Quantum Resistance 7. IANA Considerations 8. References 8.1. Normative References 8.2. Informative References Appendix A. Test Vectors A.1. ACT-Ristretto255-BLAKE3 Test Vectors A.2. Parameters A.3. Key Generation A.4. Issuance A.5. Spending A.6. Refund A.7. Refund Token A.8. ACT-P256-BLAKE3 Test Vectors A.9. Parameters A.10. Key Generation A.11. Issuance A.12. Spending A.13. Refund A.14. Refund Token Appendix B. Implementation Status B.1. anonymous-credit-tokens Appendix C. Terminology Glossary Appendix D. Acknowledgments Authors' Addresses 1. Introduction Modern web services face a fundamental tension between operational needs and user privacy. Services need to implement rate limiting to prevent abuse, charge for API usage to sustain operations, and allocate computational resources fairly. However, traditional approaches require tracking client identities and creating detailed logs of client behavior, raising significant privacy concerns in an era of increasing data protection awareness and regulation. Anonymous Credit Tokens (ACT) help to resolve this tension by providing a cryptographic protocol that enables credit-based systems without client tracking. Built on keyed-verification anonymous credentials [KVAC] and privately verifiable BBS-style signatures [BBS], the protocol allows services to issue, track, and spend credits while maintaining client privacy. 1.1. Key Properties The protocol provides four essential properties that make it suitable for privacy-preserving credit systems: 1. *Unlinkability*: The issuer cannot link credit issuance to spending, or connect multiple transactions by the same client. This property is information-theoretic, not merely computational. 2. *Partial Spending*: Clients can spend any amount up to their balance and receive anonymous change without revealing their previous or current balance, enabling flexible spending. 3. *Double-Spend Prevention*: Cryptographic nullifiers ensure each token is used only once, without linking it to issuance. 4. *Balance Privacy*: During spending, only the amount being spent is revealed, not the total balance in the token, protecting clients from balance-based profiling. 1.2. Use Cases Anonymous Credit Tokens can be applied to various scenarios: * *Rate Limiting*: Services can issue daily credit allowances that clients spend anonymously for API calls or resource access. * *API Credits*: API providers can sell credit packages that developers use to pay for API requests without creating a detailed usage history linked to their identity. This enables: - Pre-paid API access without requiring credit cards for each transaction - Anonymous API usage for privacy-sensitive applications - Usage-based billing without tracking individual request patterns - Protection against competitive analysis through usage monitoring 1.3. Protocol Overview The protocol involves two parties: an issuer (typically a service provider) and clients (typically users of the service). The interaction follows three main phases: 1. *Setup*: The issuer generates a key pair and publishes the public key. 2. *Issuance*: A client requests credits from the issuer. The issuer creates a blind signature on the credit value and a client-chosen nullifier, producing a credit token. 3. *Spending*: To spend credits, the client reveals a nullifier and proves possession of a valid token associated with that nullifier having sufficient balance. The issuer verifies the proof, checks the nullifier hasn't been used before, and issues a new token (which remains hidden from the issuer) for any remaining balance. 1.4. Design Goals The protocol is designed with the following goals: * *Privacy*: Unlinkability between issuance and spending; see the Security Properties section for the formal definition. * *Security*: Clients cannot spend more credits than they possess or use the same credits multiple times. * *Efficiency*: All operations should be computationally efficient, with performance characteristics suitable for high-volume web services and a large number of applications. * *Simplicity*: The protocol should be straightforward to implement and integrate into existing systems relative to other comparable solutions. 1.5. Relation to Existing Work This protocol builds upon several cryptographic primitives: * *BBS Signatures* [BBS]: The core signature scheme that enables efficient proofs of possession. We use a variant that is privately verifiable, which avoids the need for pairings and makes our protocol more efficient. * *Sigma Protocols* [ORRU-SIGMA]: The zero-knowledge proof framework used for spending proofs. * *Fiat-Shamir Transform* [ORRU-FS]: The technique to make the interactive proofs non-interactive. The protocol can be viewed as a specialized instantiation of keyed- verification anonymous credentials [KVAC] optimized for numerical values and partial spending. 2. Conventions and Definitions 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. 2.1. Notation This document uses the following notation: * ||: Concatenation of byte strings * x <- S: Sampling x uniformly from the set S * x := y: Assignment of the value y to the variable x * [n]: The set of integers {0, 1, ..., n-1} * |x|: The length of byte string x * 0x prefix: Hexadecimal values * We use additive notation for group operations, so group elements are added together like a + b and scalar multiplication of a group element by a scalar is written as a * n, with group element a and scalar n. 2.2. Data Types The protocol uses the following data types: * *Scalar*: An integer modulo the group order q * *Element*: An element of the ciphersuite's prime-order group * *ByteString*: A sequence of bytes 2.3. Ciphersuites This document defines two ciphersuites. All parties in a protocol session MUST agree on the same ciphersuite. Implementations MUST NOT mix parameters from different ciphersuites. 2.3.1. ACT-Ristretto255-BLAKE3 +==================+================================================+ | Parameter | Value | +==================+================================================+ | Group | Ristretto255 [RFC9496] | +------------------+------------------------------------------------+ | Element encoding | 32 bytes, compressed Ristretto point | +------------------+------------------------------------------------+ | Scalar encoding | 32 bytes, little-endian | +------------------+------------------------------------------------+ | Group order q | 2^252 + | | | 27742317777372353535851937790883648493 | +------------------+------------------------------------------------+ | Generator G | Standard Ristretto255 generator | +------------------+------------------------------------------------+ | Hash-to-group | HashToRistretto255 (OneWayMap from | | | [RFC9496] Section 4.3.4) | +------------------+------------------------------------------------+ | Protocol version | "curve25519-ristretto anonymous- | | | credits v1.0" | +------------------+------------------------------------------------+ Table 1 2.3.2. ACT-P256-BLAKE3 +=========+==================================================================+ |Parameter|Value | +=========+==================================================================+ |Group |P-256 (secp256r1) [RFC6090] | +---------+------------------------------------------------------------------+ |Element |33 bytes, SEC1 compressed point (0x02/0x03 prefix + 32 bytes) | |encoding |[SEC1] | +---------+------------------------------------------------------------------+ |Scalar |32 bytes, big-endian | |encoding | | +---------+------------------------------------------------------------------+ |Group |0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551| |order q | | +---------+------------------------------------------------------------------+ |Generator|Standard P-256 generator | |G | | +---------+------------------------------------------------------------------+ |Hash-to- |HashToP256 (scalar reduction + generator multiplication) | |group | | +---------+------------------------------------------------------------------+ |Protocol |"p256 anonymous-credits v1.0" | |version | | +---------+------------------------------------------------------------------+ Table 2 2.4. Cryptographic Parameters The protocol is defined generically over a prime-order group. The ciphersuite determines the concrete group and encoding. The key parameters are: * *q*: The prime order of the group (ciphersuite-dependent, see above) * *G*: The standard generator of the group (ciphersuite-dependent) * *L*: The bit length for credit values 3. Protocol Specification 3.1. System Parameters The protocol requires the following system parameters: Parameters: - G: Generator of the ciphersuite's group - H1, H2, H3, H4: Additional generators for commitments - L: Bit length for credit values (configurable, must satisfy 1 <= L <= 128) Implementations MUST enforce 1 <= L <= 128. See the Parameter Selection section for the rationale behind this constraint. The generators H1, H2, H3, and H4 MUST be generated deterministically from a nothing-up-my-sleeve value to ensure they are independent of each other and of G. This prevents attacks whereby malicious parameters could compromise security. Note that these generators are independent of the choice of L. GenerateParameters(domain_separator): Input: - domain_separator: ByteString identifying the deployment Output: - params: System parameters (H1, H2, H3, H4) Steps: 1. seed = BLAKE3(LengthPrefixed(domain_separator)) 2. counter = 0 3. H1 = HashToGroup(seed, counter++) 4. H2 = HashToGroup(seed, counter++) 5. H3 = HashToGroup(seed, counter++) 6. H4 = HashToGroup(seed, counter++) 7. return (H1, H2, H3, H4) The HashToGroup function is ciphersuite-dependent: *ACT-Ristretto255-BLAKE3:* HashToRistretto255(seed, counter): Input: - seed: 32-byte seed value - counter: Integer counter for domain separation Output: - P: A valid Ristretto255 point Steps: 1. hasher = BLAKE3.new() 2. hasher.update(LengthPrefixed(domain_separator)) 3. hasher.update(LengthPrefixed(seed)) 4. hasher.update(LengthPrefixed(counter.to_le_bytes(4))) 5. uniform_bytes = hasher.finalize_xof(64) 6. P = OneWayMap(uniform_bytes) 7. return P *ACT-P256-BLAKE3:* HashToP256(seed, counter): Input: - seed: 32-byte seed value - counter: Integer counter for domain separation Output: - P: A valid P-256 point Steps: 1. hasher = BLAKE3.new() 2. hasher.update(LengthPrefixed(domain_separator)) 3. hasher.update(LengthPrefixed(seed)) 4. hasher.update(LengthPrefixed(counter.to_le_bytes(4))) 5. uniform_bytes = hasher.finalize_xof(32) 6. s = from_big_endian_bytes(uniform_bytes) mod q 7. P = G * s 8. return P Note: For P-256, 32 bytes of XOF output provides negligible bias (less than 2^-128) when reduced modulo the ~2^256 group order. The domain_separator MUST be unique for each deployment to ensure cryptographic isolation between different services. The domain separator SHOULD follow this structured format: domain_separator = "ACT-v1:" || organization || ":" || service || ":" || deployment_id || ":" || version Each component (organization, service, deployment_id, version) MUST NOT contain the colon character ':'. Where: * organization: A unique identifier for the organization (e.g., "example-corp", "acme-inc") * service: The specific service or application name (e.g., "payment- api", "rate-limiter") * deployment_id: The deployment environment (e.g., "production", "staging", "us-west-1") * version: An ISO 8601 date (YYYY-MM-DD) indicating when parameters were generated Example: "ACT-v1:example-corp:payment-api:production:2024-01-15" This structured format ensures: 1. Protocol identification through the "ACT-v1:" prefix 2. Organizational namespace isolation 3. Service-level separation within organizations 4. Environment isolation (production vs staging) 5. Version tracking for parameter updates Using generic or unstructured domain separators creates security risks through parameter collision and MUST NOT be used. When parameters need to be updated (e.g., for security reasons or protocol upgrades), a new version date MUST be used, creating entirely new parameters. For ACT-Ristretto255-BLAKE3, the OneWayMap function is defined in [RFC9496] Section 4.3.4, which provides a cryptographically secure mapping from uniformly random byte strings to valid Ristretto255 points. For ACT-P256-BLAKE3, the hash-to-group function uses scalar reduction followed by generator multiplication. This is secure because the resulting point distribution is statistically close to uniform over the group. 3.2. Key Generation The issuer generates a key pair as follows: KeyGen(): Input: None Output: - sk: Private key (Scalar) - pk: Public key (Group Element) Steps: 1. x <- Zq 2. W = G * x 3. sk = x 4. pk = W 5. return (sk, pk) 3.3. Token Issuance The issuance protocol is an interactive protocol between a client and the issuer: 3.3.1. Client: Issuance Request IssueRequest(): Output: - request: Issuance request - state: Client state for later verification Steps: 1. k <- Zq // Nullifier (will prevent double-spending) 2. r <- Zq // Blinding factor 3. K = H2 * k + H3 * r 4. // Generate proof of knowledge of k, r 5. k' <- Zq 6. r' <- Zq 7. K1 = H2 * k' + H3 * r' 8. transcript = CreateTranscript("request") 9. AddToTranscript(transcript, K) 10. AddToTranscript(transcript, K1) 11. gamma = GetChallenge(transcript) 12. k_bar = k' + gamma * k 13. r_bar = r' + gamma * r 14. request = (K, gamma, k_bar, r_bar) 15. state = (k, r) 16. return (request, state) 3.3.2. Issuer: Issuance Response IssueResponse(sk, request, c, ctx): Input: - sk: Issuer's private key - request: Client's issuance request - c: Credit amount to issue (c > 0) - ctx: Request context (Scalar) Output: - response: Issuance response or INVALID Exceptions: - InvalidIssuanceRequestProof, raised when the client proof verification fails Steps: 1. Parse request as (K, gamma, k_bar, r_bar) 2. // Verify proof of knowledge 3. K1 = H2 * k_bar + H3 * r_bar - K * gamma 4. transcript = CreateTranscript("request") 5. AddToTranscript(transcript, K) 6. AddToTranscript(transcript, K1) 7. if GetChallenge(transcript) != gamma: 8. raise InvalidIssuanceRequestProof 9. // Create BBS signature on (c, ctx, k, r) 10. e <- Zq 11. A = (G + H1 * c + H4 * ctx + K) * (1/(e + sk)) // K = H2 * k + H3 * r 12. // Generate proof of correct computation 13. alpha <- Zq 14. Y_A = A * alpha 15. Y_G = G * alpha 16. X_A = G + H1 * c + H4 * ctx + K 17. X_G = G * e + pk 18. transcript_resp = CreateTranscript("respond") 19. AddToTranscript(transcript_resp, c) 20. AddToTranscript(transcript_resp, ctx) 21. AddToTranscript(transcript_resp, e) 22. AddToTranscript(transcript_resp, A) 23. AddToTranscript(transcript_resp, X_A) 24. AddToTranscript(transcript_resp, X_G) 25. AddToTranscript(transcript_resp, Y_A) 26. AddToTranscript(transcript_resp, Y_G) 27. gamma_resp = GetChallenge(transcript_resp) 28. z = gamma_resp * (sk + e) + alpha 29. response = (A, e, gamma_resp, z, c, ctx) 30. return response 3.3.3. Client: Token Verification VerifyIssuance(pk, request, response, state): Input: - pk: Issuer's public key - request: The issuance request sent - response: Issuer's response - state: Client state from request generation Output: - token: Credit token Exceptions: - InvalidIssuanceResponseProof, raised when the server proof verification fails Steps: 1. Parse request as (K, gamma, k_bar, r_bar) 2. Parse response as (A, e, gamma_resp, z, c, ctx) 3. Parse state as (k, r) 4. // Verify proof 5. X_A = G + H1 * c + H4 * ctx + K 6. X_G = G * e + pk 7. Y_A = A * z - X_A * gamma_resp 8. Y_G = G * z - X_G * gamma_resp 9. transcript_resp = CreateTranscript("respond") 10. AddToTranscript(transcript_resp, c) 11. AddToTranscript(transcript_resp, ctx) 12. AddToTranscript(transcript_resp, e) 13. AddToTranscript(transcript_resp, A) 14. AddToTranscript(transcript_resp, X_A) 15. AddToTranscript(transcript_resp, X_G) 16. AddToTranscript(transcript_resp, Y_A) 17. AddToTranscript(transcript_resp, Y_G) 18. if GetChallenge(transcript_resp) != gamma_resp: 19. raise InvalidIssuanceResponseProof 20. token = (A, e, k, r, c, ctx) 21. return token 3.4. Token Spending The spending protocol allows a client to spend s credits from a token containing c credits (where 0 <= s <= c). Note: Spending s = 0 is permitted and produces a new token with the same balance but a fresh nullifier. This "re-anonymization" operation is useful for securely transferring a token to another party: after a zero-spend, the original holder can no longer use the old nullifier, and the recipient obtains a token that is cryptographically unlinkable to the original. 3.4.1. Client: Spend Proof Generation ProveSpend(token, s): Input: - token: Credit token (A, e, k, r, c, ctx) - s: Amount to spend (0 <= s <= c) Output: - proof: Spend proof - state: Client state for receiving change Exceptions: - InvalidAmount: raised when s > c or s >= 2^L or c >= 2^L Steps: 1. // Validate inputs 2. if s >= 2^L: 3. raise InvalidAmount 4. if c >= 2^L: 5. raise InvalidAmount 6. if s > c: 7. raise InvalidAmount 8. // Randomize the signature 9. r1, r2 <- Zq 10. B = G + H1 * c + H2 * k + H3 * r + H4 * ctx 11. A' = A * (r1 * r2) 12. B_bar = B * r1 13. r3 = 1/r1 14. // Generate initial proof components 15. c' <- Zq 16. r' <- Zq 17. e' <- Zq 18. r2' <- Zq 19. r3' <- Zq 20. // Compute first round messages 21. A1 = A' * e' + B_bar * r2' 22. A2 = B_bar * r3' + H1 * c' + H3 * r' 23. // Decompose c - s into bits 24. m = c - s 25. (i[0], ..., i[L-1]) = BitDecompose(m) // See Section 3.7 26. // Create commitments for each bit 27. k* <- Zq 28. s[0] <- Zq 29. Com[0] = H1 * i[0] + H2 * k* + H3 * s[0] 30. For j = 1 to L-1: 31. s[j] <- Zq 32. Com[j] = H1 * i[j] + H3 * s[j] 33. // Initialize range proof arrays 34. C = array[L][2] 35. C' = array[L][2] 36. gamma0 = array[L] 37. z = array[L][2] 38. // Process bit 0 (with k* component) 39. C[0][0] = Com[0] 40. C[0][1] = Com[0] - H1 41. k0' <- Zq 42. s_prime = array[L] 43. s_prime[0] <- Zq 44. gamma0[0] <- Zq 45. w0 <- Zq 46. z[0] <- Zq 47. if i[0] == 0: 48. C'[0][0] = H2 * k0' + H3 * s_prime[0] 49. C'[0][1] = H2 * w0 + H3 * z[0] - C[0][1] * gamma0[0] 50. else: 51. C'[0][0] = H2 * w0 + H3 * z[0] - C[0][0] * gamma0[0] 52. C'[0][1] = H2 * k0' + H3 * s_prime[0] 53. // Process remaining bits 54. For j = 1 to L-1: 55. C[j][0] = Com[j] 56. C[j][1] = Com[j] - H1 57. s_prime[j] <- Zq 58. gamma0[j] <- Zq 59. z[j] <- Zq 60. 61. if i[j] == 0: 62. C'[j][0] = H3 * s_prime[j] 63. C'[j][1] = H3 * z[j] - C[j][1] * gamma0[j] 64. else: 65. C'[j][0] = H3 * z[j] - C[j][0] * gamma0[j] 66. C'[j][1] = H3 * s_prime[j] 67. // Compute K' commitment 68. K' = Sum(Com[j] * 2^j for j in [L]) 69. r* = Sum(s[j] * 2^j for j in [L]) 70. k' <- Zq 71. s' <- Zq 72. C_final = H1 * (-c') + H2 * k' + H3 * s' 73. // Generate challenge using transcript 74. transcript = CreateTranscript("spend") 75. AddToTranscript(transcript, k) 76. AddToTranscript(transcript, ctx) 77. AddToTranscript(transcript, A') 78. AddToTranscript(transcript, B_bar) 79. AddToTranscript(transcript, A1) 80. AddToTranscript(transcript, A2) 81. For j = 0 to L-1: 82. AddToTranscript(transcript, Com[j]) 83. For j = 0 to L-1: 84. AddToTranscript(transcript, C'[j][0]) 85. AddToTranscript(transcript, C'[j][1]) 86. AddToTranscript(transcript, C_final) 87. gamma = GetChallenge(transcript) 88. // Compute responses 89. e_bar = -gamma * e + e' 90. r2_bar = gamma * r2 + r2' 91. r3_bar = gamma * r3 + r3' 92. c_bar = -gamma * c + c' 93. r_bar = -gamma * r + r' 94. // Complete range proof responses 95. z_final = array[L][2] 96. gamma0_final = array[L] 97. 98. // For bit 0 99. if i[0] == 0: 100. gamma0_final[0] = gamma - gamma0[0] 101. w00 = gamma0_final[0] * k* + k0' 102. w01 = w0 103. z_final[0][0] = gamma0_final[0] * s[0] + s_prime[0] 104. z_final[0][1] = z[0] 105. else: 106. gamma0_final[0] = gamma0[0] 107. w00 = w0 108. w01 = (gamma - gamma0_final[0]) * k* + k0' 109. z_final[0][0] = z[0] 110. z_final[0][1] = (gamma - gamma0_final[0]) * s[0] + s_prime[0] 111. // For remaining bits 112. For j = 1 to L-1: 113. if i[j] == 0: 114. gamma0_final[j] = gamma - gamma0[j] 115. z_final[j][0] = gamma0_final[j] * s[j] + s_prime[j] 116. z_final[j][1] = z[j] 117. else: 118. gamma0_final[j] = gamma0[j] 119. z_final[j][0] = z[j] 120. z_final[j][1] = (gamma - gamma0_final[j]) * s[j] + s_prime[j] 121. k_bar = gamma * k* + k' 122. s_bar = gamma * r* + s' 123. // Construct proof 124. proof = (k, s, ctx, A', B_bar, Com, gamma, e_bar, 125. r2_bar, r3_bar, c_bar, r_bar, 126. w00, w01, gamma0_final, z_final, 127. k_bar, s_bar) 128. state = (k*, r*, m, ctx) 129. return (proof, state) 3.4.2. Issuer: Spend Verification and Refund VerifyAndRefund(sk, proof, t): Input: - sk: Issuer's private key - proof: Client's spend proof - t: Credits to return to the client (0 <= t <= s, t < 2^L) Output: - refund: Refund for remaining credits Exceptions: - DoubleSpendError: raised when the nullifier has been used before - InvalidSpendProof: raised when the spend proof verification fails Steps: 1. Parse proof and extract nullifier k 2. // Check nullifier hasn't been used 3. if k in used_nullifiers: 4. raise DoubleSpendError 5. // Verify the proof (see Section 3.5.2) 6. if not VerifySpendProof(sk, proof): 7. raise InvalidSpendProof 8. // Record nullifier 9. used_nullifiers.add(k) 10. // Issue refund for remaining balance 11. K' = Sum(Com[j] * 2^j for j in [L]) 12. refund = IssueRefund(sk, K', proof.ctx, proof.s, t) 13. return refund 3.4.3. Refund Issuance After verifying a spend proof, the issuer creates a refund token for the remaining balance. The issuer may optionally return t credits (where 0 <= t <= s) back to the client via a partial credit return. This enables pre-authorization patterns where the client holds s credits but only t are returned unused. The resulting token will have c - s + t credits. Use t = 0 to consume the full spend amount: IssueRefund(sk, K', ctx, s, t): Input: - sk: Issuer's private key - K': Commitment to remaining balance and new nullifier - ctx: Request context from the spend proof - s: The spend amount from the proof - t: Credits to return to the client (0 <= t <= s, t < 2^L) Output: - refund: Refund response Exceptions: - InvalidAmount: raised when t > s or t >= 2^L Steps: 1. // Validate partial return amount 2. if t >= 2^L: 3. raise InvalidAmount 4. if t > s: 5. raise InvalidAmount 6. // Create new BBS signature on remaining balance + partial return 7. e* <- Zq 8. X_A* = G + K' + H1 * t + H4 * ctx 9. A* = X_A* * (1/(e* + sk)) 10. // Generate proof of correct computation 11. alpha <- Zq 12. Y_A = A* * alpha 13. Y_G = G * alpha 14. X_G = G * e* + pk 15. // Create challenge using transcript 16. transcript = CreateTranscript("refund") 17. AddToTranscript(transcript, e*) 18. AddToTranscript(transcript, t) 19. AddToTranscript(transcript, ctx) 20. AddToTranscript(transcript, A*) 21. AddToTranscript(transcript, X_A*) 22. AddToTranscript(transcript, X_G) 23. AddToTranscript(transcript, Y_A) 24. AddToTranscript(transcript, Y_G) 25. gamma = GetChallenge(transcript) 26. // Compute response 27. z = gamma * (sk + e*) + alpha 28. refund = (A*, e*, gamma, z, t) 29. return refund 3.4.4. Client: Refund Token Construction The client verifies the refund and constructs a new credit token: ConstructRefundToken(pk, spend_proof, refund, state): Input: - pk: Issuer's public key - spend_proof: The spend proof sent to issuer - refund: Issuer's refund response - state: Client state (k*, r*, m, ctx) Output: - token: New credit token or INVALID Exceptions: - InvalidRefundProof: When the refund proof verification fails Steps: 1. Parse refund as (A*, e*, gamma, z, t) 2. Parse state as (k*, r*, m, ctx) 3. // Reconstruct commitment with partial return 4. K' = Sum(spend_proof.Com[j] * 2^j for j in [L]) 5. X_A* = G + K' + H1 * t + H4 * ctx 6. X_G = G * e* + pk 7. // Verify proof 8. Y_A = A* * z + X_A* * (-gamma) 9. Y_G = G * z + X_G * (-gamma) 10. // Check challenge using transcript 11. transcript = CreateTranscript("refund") 12. AddToTranscript(transcript, e*) 13. AddToTranscript(transcript, t) 14. AddToTranscript(transcript, ctx) 15. AddToTranscript(transcript, A*) 16. AddToTranscript(transcript, X_A*) 17. AddToTranscript(transcript, X_G) 18. AddToTranscript(transcript, Y_A) 19. AddToTranscript(transcript, Y_G) 20. if GetChallenge(transcript) != gamma: 21. raise InvalidRefundProof 22. // Construct new token with remaining balance + partial return 23. token = (A*, e*, k*, r*, m + t, ctx) 24. return token 3.4.5. Spend Proof Verification The issuer verifies a spend proof as follows: VerifySpendProof(sk, proof): Input: - sk: Issuer's private key - proof: Spend proof from client Output: - valid: Boolean indicating if proof is valid Exceptions: - IdentityPointError: raised when A' is the identity - InvalidClientSpendProof: raised when the challenge does not match the reconstruction Steps: 1. Parse proof as (k, s, ctx, A', B_bar, Com, gamma, e_bar, r2_bar, r3_bar, c_bar, r_bar, w00, w01, gamma0, z, k_bar, s_bar) 2. // Check A' is not identity 3. if A' == Identity: 4. raise IdentityPointError 5. // Compute issuer's view of signature 6. A_bar = A' * sk 7. H1_prime = G + H2 * k + H4 * ctx 8. // Verify sigma protocol 9. A1 = A' * e_bar + B_bar * r2_bar - A_bar * gamma 10. A2 = B_bar * r3_bar + H1 * c_bar + H3 * r_bar - H1_prime * gamma 11. // Initialize arrays for range proof verification 12. gamma1 = array[L] 13. C = array[L][2] 14. C' = array[L][2] 15. // Process bit 0 (with k* component) 16. gamma1[0] = gamma - gamma0[0] 17. C[0][0] = Com[0] 18. C[0][1] = Com[0] - H1 19. C'[0][0] = H2 * w00 + H3 * z[0][0] - C[0][0] * gamma0[0] 20. C'[0][1] = H2 * w01 + H3 * z[0][1] - C[0][1] * gamma1[0] 21. // Verify remaining bits 22. For j = 1 to L-1: 23. gamma1[j] = gamma - gamma0[j] 24. C[j][0] = Com[j] 25. C[j][1] = Com[j] - H1 26. C'[j][0] = H3 * z[j][0] - C[j][0] * gamma0[j] 27. C'[j][1] = H3 * z[j][1] - C[j][1] * gamma1[j] 28. // Verify final commitment 29. K' = Sum(Com[j] * 2^j for j in [L]) 30. Com_total = H1 * s + K' 31. C_final = H1 * (-c_bar) + H2 * k_bar + H3 * s_bar - Com_total * gamma 32. // Recompute challenge using transcript 33. transcript = CreateTranscript("spend") 34. AddToTranscript(transcript, k) 35. AddToTranscript(transcript, ctx) 36. AddToTranscript(transcript, A') 37. AddToTranscript(transcript, B_bar) 38. AddToTranscript(transcript, A1) 39. AddToTranscript(transcript, A2) 40. For j = 0 to L-1: 41. AddToTranscript(transcript, Com[j]) 42. For j = 0 to L-1: 43. AddToTranscript(transcript, C'[j][0]) 44. AddToTranscript(transcript, C'[j][1]) 45. AddToTranscript(transcript, C_final) 46. gamma_check = GetChallenge(transcript) 47. // Verify challenge matches 48. if gamma != gamma_check: 49. raise InvalidVerifySpendProof 50. return true 3.5. Cryptographic Primitives 3.5.1. Protocol Version The protocol version string for domain separation is ciphersuite- dependent: ACT-Ristretto255-BLAKE3: PROTOCOL_VERSION = "curve25519-ristretto anonymous-credits v1.0" ACT-P256-BLAKE3: PROTOCOL_VERSION = "p256 anonymous-credits v1.0" The version string MUST be used consistently across all implementations of a given ciphersuite for interoperability. The curve specification is included to prevent cross-curve attacks and ensure implementations using different curves cannot accidentally interact. 3.5.2. Hash Function and Fiat-Shamir Transform The protocol uses BLAKE3 [BLAKE3] as the underlying hash function for the Fiat-Shamir transform [ORRU-FS]. Following the sigma protocol framework [ORRU-SIGMA], challenges are generated using a transcript that accumulates all protocol messages: CreateTranscript(label): Input: - label: ASCII string identifying the proof type Output: - transcript: A new transcript object Steps: 1. hasher = BLAKE3.new() 2. hasher.update(LengthPrefixed(PROTOCOL_VERSION)) 3. hasher.update(LengthPrefixed(Encode(H1))) 4. hasher.update(LengthPrefixed(Encode(H2))) 5. hasher.update(LengthPrefixed(Encode(H3))) 6. hasher.update(LengthPrefixed(Encode(H4))) 7. hasher.update(LengthPrefixed(label)) 8. return transcript with hasher AddToTranscript(transcript, value): Input: - transcript: Existing transcript - value: Element or Scalar to add Steps: 1. encoded = Encode(value) 2. transcript.hasher.update(LengthPrefixed(encoded)) GetChallenge(transcript): Input: - transcript: Completed transcript Output: - challenge: Scalar challenge value ACT-Ristretto255-BLAKE3: 1. hash = transcript.hasher.output(64) // 64 bytes of output 2. challenge = from_little_endian_bytes(hash) mod q 3. return challenge ACT-P256-BLAKE3: 1. hash = transcript.hasher.output(32) // 32 bytes of output 2. challenge = from_big_endian_bytes(hash) mod q 3. return challenge Note: For ACT-P256-BLAKE3, 32 bytes of XOF output provides negligible bias (less than 2^-128) when reduced modulo the ~2^256 group order. This approach ensures: * Domain separation through the label and protocol version * Inclusion of all public parameters to prevent parameter substitution attacks * Proper ordering with length prefixes to prevent ambiguity * Deterministic challenge generation from the complete transcript 3.5.3. Encoding Functions Elements and scalars are encoded according to the ciphersuite: Encode(value): Input: - value: Element or Scalar Output: - encoding: ByteString ACT-Ristretto255-BLAKE3: 1. If value is an Element: 2. return value.compress() // 32 bytes, compressed Ristretto point 3. If value is a Scalar: 4. return value.to_bytes_le() // 32 bytes, little-endian ACT-P256-BLAKE3: 1. If value is an Element: 2. return SEC1_compressed(value) // 33 bytes, 0x02/0x03 + 32 bytes 3. If value is a Scalar: 4. return value.to_bytes_be() // 32 bytes, big-endian The following function provides consistent length-prefixing for hash inputs: LengthPrefixed(data): Input: - data: ByteString to be length-prefixed Output: - prefixed: ByteString with length prefix Steps: 1. length = len(data) 2. return length.to_be_bytes(8) || data // 8-byte big-endian length prefix Note: Implementations MAY use standard serialization formats (e.g. CBOR) for complex structures, but MUST ensure deterministic encoding for hash inputs. 3.5.4. Binary Decomposition To decompose a scalar into its binary representation: BitDecompose(s): Input: - s: Scalar value Output: - bits: Array of L scalars (each 0 or 1) ACT-Ristretto255-BLAKE3: 1. bytes = s.to_bytes_le() // 32 bytes, little-endian 2. For i = 0 to L-1: 3. byte_index = i / 8 4. bit_position = i % 8 5. bit = (bytes[byte_index] >> bit_position) & 1 6. bits[i] = Scalar(bit) 7. return bits ACT-P256-BLAKE3: 1. bytes = s.to_bytes_be() // 32 bytes, big-endian 2. For i = 0 to L-1: 3. byte_index = 31 - (i / 8) 4. bit_position = i % 8 5. bit = (bytes[byte_index] >> bit_position) & 1 6. bits[i] = Scalar(bit) 7. return bits Note: Both variants produce bits in LSB-first order (i.e., bits[0] is the least significant bit). See Section 3.1 for constraints on L. 3.5.5. Scalar Conversion Converting between credit amounts and scalars: CreditToScalar(amount): Input: - amount: Integer credit amount (0 <= amount < 2^L) Output: - s: Scalar representation Exceptions: - AmountTooBigError: raised when the amount exceeds 2^L Steps: 1. if amount >= 2^L: 2. return AmountTooBigError 3. return Scalar(amount) ScalarToCredit(s): Input: - s: Scalar value Output: - amount: Integer credit amount or ERROR Exceptions: - ScalarOutOfRangeError: raised when the scalar value is >= 2^L Steps: 1. amount = s as integer // Interpret scalar bytes as integer // (little-endian for Ristretto255, big-endian for P-256) 2. if amount >= 2^L: 3. return ScalarOutOfRangeError 4. return amount 4. Protocol Messages and Wire Format 4.1. Message Encoding All protocol messages SHOULD be encoded using deterministic CBOR (RFC 8949) for interoperability. Decoders MUST reject messages containing unknown CBOR map keys. The following sections define the structure of each message type. 4.1.1. Issuance Request Message IssuanceRequestMsg = { 1: bstr, ; K (compressed point, 32 bytes for Ristretto255 / 33 bytes for P-256) 2: bstr, ; gamma (scalar, 32 bytes) 3: bstr, ; k_bar (scalar, 32 bytes) 4: bstr ; r_bar (scalar, 32 bytes) } 4.1.2. Issuance Response Message IssuanceResponseMsg = { 1: bstr, ; A (compressed point, 32 bytes for Ristretto255 / 33 bytes for P-256) 2: bstr, ; e (scalar, 32 bytes) 3: bstr, ; gamma_resp (scalar, 32 bytes) 4: bstr, ; z (scalar, 32 bytes) 5: bstr, ; c (scalar, 32 bytes) 6: bstr ; ctx (scalar, 32 bytes) } 4.1.3. Spend Proof Message SpendProofMsg = { 1: bstr, ; k (nullifier, 32 bytes) 2: bstr, ; s (spend amount, 32 bytes) 3: bstr, ; A' (compressed point, 32/33 bytes) 4: bstr, ; B_bar (compressed point, 32/33 bytes) 5: [* bstr], ; Com array (L compressed points, 32/33 bytes each) 6: bstr, ; gamma (scalar, 32 bytes) 7: bstr, ; e_bar (scalar, 32 bytes) 8: bstr, ; r2_bar (scalar, 32 bytes) 9: bstr, ; r3_bar (scalar, 32 bytes) 10: bstr, ; c_bar (scalar, 32 bytes) 11: bstr, ; r_bar (scalar, 32 bytes) 12: bstr, ; w00 (scalar, 32 bytes) 13: bstr, ; w01 (scalar, 32 bytes) 14: [* bstr], ; gamma0 array (L scalars) 15: [* [bstr, bstr]], ; z array (L pairs of scalars) 16: bstr, ; k_bar (scalar, 32 bytes) 17: bstr, ; s_bar (scalar, 32 bytes) 18: bstr ; ctx (scalar, 32 bytes) } 4.1.4. Refund Message RefundMsg = { 1: bstr, ; A* (compressed point, 32 bytes for Ristretto255 / 33 bytes for P-256) 2: bstr, ; e* (scalar, 32 bytes) 3: bstr, ; gamma (scalar, 32 bytes) 4: bstr, ; z (scalar, 32 bytes) 5: bstr ; t (partial return, scalar, 32 bytes) } 4.2. Error Responses Error responses SHOULD use the following format: ErrorMsg = { 1: uint, ; error_code 2: tstr ; error_message (for debugging only) } Error codes are defined in Section 5.3. 4.3. Key Serialization The following formats define the serialization of issuer keys. Implementations that need to persist or transmit keys SHOULD use these formats for interoperability. 4.3.1. Public Key PublicKey = bstr ; W (compressed Ristretto point, 32 bytes) 4.3.2. Private Key PrivateKey = { 1: bstr, ; x (secret scalar, 32 bytes) 2: bstr ; W (public key point, 32 bytes) } Decoders MUST verify that W == G * x upon deserialization to prevent use of inconsistent key material. 4.4. Client State Serialization The following formats define the serialization of client-side state that must be persisted between protocol steps. Implementations that need to store or transmit client state SHOULD use these formats for interoperability. 4.4.1. Pre-Issuance State The client MUST persist this state after generating an issuance request and before receiving the issuance response. PreIssuance = { 1: bstr, ; r (blinding factor, scalar, 32 bytes) 2: bstr ; k (nullifier, scalar, 32 bytes) } 4.4.2. Credit Token The client MUST persist the credit token after issuance or refund. CreditToken = { 1: bstr, ; A (BBS signature point, compressed Ristretto, 32 bytes) 2: bstr, ; e (signature scalar, 32 bytes) 3: bstr, ; k (nullifier, scalar, 32 bytes) 4: bstr, ; r (blinding factor, scalar, 32 bytes) 5: bstr, ; c (credit amount, scalar, 32 bytes) 6: bstr ; ctx (request context, scalar, 32 bytes) } 4.4.3. Pre-Refund State The client MUST persist this state after generating a spend proof and before receiving the refund response. PreRefund = { 1: bstr, ; r (blinding factor, scalar, 32 bytes) 2: bstr, ; k (nullifier, scalar, 32 bytes) 3: bstr, ; m (remaining balance, scalar, 32 bytes) 4: bstr ; ctx (request context, scalar, 32 bytes) } 4.5. Protocol Flow The complete protocol flow with message types: Client Issuer | | |-- IssuanceRequestMsg ------------------------>| | | |<-- IssuanceResponseMsg -----------------------| | | | (client creates token) | | | |-- SpendProofMsg ----------------------------->| | | |<-- RefundMsg or ErrorMsg ---------------------| | | 4.5.1. Example Usage Scenario Consider an API service that sells credits in bundles of 1000: 1. *Purchase*: Alice buys 1000 API credits * Alice generates a random nullifier k and blinding factor r * Alice sends IssuanceRequestMsg to the service * Service creates a BBS signature on (1000, k, r) and returns it * Alice now has a token worth 1000 credits 2. *First API Call*: Alice makes an API call costing 50 credits * Alice creates a SpendProofMsg proving she has ≥ 50 credits * Alice reveals nullifier k to prevent double-spending * Service verifies the proof and records k as used * Service issues a RefundMsg for a new token worth 950 credits * Alice generates new nullifier k' for the refund token 3. *Subsequent Calls*: Alice continues using the API * Each call repeats the spend/refund process * Each new token has a fresh nullifier * The service cannot link Alice's calls together This example demonstrates how the protocol maintains privacy while preventing double-spending and enabling flexible partial payments. 5. Implementation Considerations 5.1. Nullifier Management Implementations MUST maintain a persistent database of used nullifiers to prevent double-spending. The nullifier storage requirements grow linearly with the number of spent tokens. Implementations MAY use the following strategies to manage storage: 1. *Expiration*: If tokens have expiration dates, old nullifiers can be pruned. 2. *Sharding*: Nullifiers can be partitioned across multiple databases. 3. *Bloom Filters*: Probabilistic data structures can reduce memory usage with a small false-positive rate. WARNING: false positives cause legitimate spends to be rejected. Bloom filters MUST NOT be the sole nullifier check; a positive result MUST be confirmed against authoritative storage before rejecting a spend. 5.2. Constant-Time Operations Implementations MUST use constant-time operations for all secret- dependent computations. See the Security Considerations section for detailed requirements and mitigations. 5.3. Randomness Generation The security of the protocol critically depends on the quality of random number generation. Implementations MUST use cryptographically secure random number generators (CSPRNGs) for: * Private key generation * Blinding factors (r, k) * Proof randomness (nonces) 5.3.1. RNG Requirements 1. *Entropy Source*: Use OS-provided entropy (e.g., /dev/urandom on Unix systems) 2. *Fork Safety*: Reseed after fork() to prevent nonce reuse 3. *Backtracking Resistance*: Use forward-secure PRNGs when possible 5.3.2. Nonce Generation Following [ORRU-SIGMA], nonces (the randomness used in proofs) MUST be generated with extreme care: 1. *Fresh Randomness*: Generate new nonces for every proof 2. *No Reuse*: Never reuse nonces across different proofs 3. *Full Entropy*: Use the full security parameter (256 bits) of randomness 4. *Zeroization*: Clear nonces from memory after use WARNING: Leakage of even a few bits of a nonce can allow complete recovery of the witness (secret values). Implementations MUST use constant-time operations and secure memory handling for all nonce- related computations. 5.4. Point Validation All group elements received from external sources MUST be validated: *ACT-Ristretto255-BLAKE3:* 1. *Deserialization*: Verify the point deserializes to a valid Ristretto point 2. *Non-Identity*: Verify the point is not the identity element 3. *Subgroup Check*: Ristretto guarantees prime-order subgroup membership *ACT-P256-BLAKE3:* 1. *Deserialization*: Verify the SEC1 compressed encoding is valid and the point lies on the P-256 curve 2. *Non-Identity*: Verify the point is not the identity element (point at infinity) 3. *Subgroup Check*: P-256 has cofactor 1, so all curve points are in the prime-order subgroup Example validation: ValidatePoint(P): 1. If P fails to deserialize: 2. return INVALID 3. If P == Identity: 4. return INVALID 5. // Both Ristretto255 and P-256 guarantee prime-order subgroup membership 6. return VALID All implementations MUST validate points at these locations: * When receiving K in issuance request * When receiving A in issuance response * When receiving A' and B_bar in spend proof * When receiving Com[j] commitments in spend proof * When receiving A* in refund response 5.5. Error Handling Implementations SHOULD NOT provide detailed error messages that could leak information about the verification process. A single INVALID response should be returned for all verification failures. 5.5.1. Error Codes While detailed error messages should not be exposed to untrusted parties, implementations MAY use the following internal error codes: * INVALID_PROOF: Proof verification failed * NULLIFIER_REUSE: Double-spend attempt detected * MALFORMED_REQUEST: Request format is invalid * INVALID_AMOUNT: Credit amount is invalid (exceeds 2^L - 1, spend exceeds balance, or partial return exceeds spend) 5.6. Parameter Selection Implementations MUST choose L based on their maximum credit requirements and performance constraints. See Section 3.1 for constraints on L. The bit length L is configurable and determines the range of credit values (0 to 2^L - 1). The choice of L involves several trade-offs: 1. *Range*: Larger L supports higher credit values 2. *Performance*: Proof size and verification time scale linearly with L 5.6.1. Performance Characteristics The protocol has the following computational complexity: *Notation for Operations:* * *Group Operations*: Point additions in the ciphersuite's group (e.g., P + Q) * *Group Exponentiations*: Scalar multiplication of group elements (e.g., P * s) * *Scalar Additions/Multiplications*: Arithmetic operations modulo the group order q * *Issuance*: +============+==========+===============+=========+===============+======+ |Operation |Group |Group |Scalar |Scalar |Hashes| | |Operations|Exponentiations|Additions|Multiplications| | +============+==========+===============+=========+===============+======+ |Client |2 |4 |2 |1 |1 | |Request | | | | | | +------------+----------+---------------+---------+---------------+------+ |Issuer |5 |8 |3 |1 |2 | |Response | | | | | | +------------+----------+---------------+---------+---------------+------+ |Client |5 |5 |0 |0 |1 | |Credit Token| | | | | | |Construction| | | | | | +------------+----------+---------------+---------+---------------+------+ Table 3 * *Spending*: +============+==========+===============+=========+===============+======+ |Operation |Group |Group |Scalar |Scalar |Hashes| | |Operations|Exponentiations|Additions|Multiplications| | +============+==========+===============+=========+===============+======+ |Client |17 + 4L |27 + 8L |13 + 5L |12 + 3L |1 | |Request | | | | | | +------------+----------+---------------+---------+---------------+------+ |Issuer |16 + 4L |24 + 5L |4 + L |1 |1 | |Response | | | | | | +------------+----------+---------------+---------+---------------+------+ |Client |3 |5 |L |L |1 | |Credit Token| | | | | | |Construction| | | | | | +------------+----------+---------------+---------+---------------+------+ Table 4 Note: L is the configurable bit length for credit values. * *Storage* (ACT-Ristretto255-BLAKE3): +==========================+==========================+ | Component | Size | +==========================+==========================+ | Token size | 192 bytes (6 × 32 bytes) | +--------------------------+--------------------------+ | Spend proof size | 32 × (14 + 4L) bytes | +--------------------------+--------------------------+ | Nullifier database entry | 32 bytes per spent token | +--------------------------+--------------------------+ Table 5 * *Storage* (ACT-P256-BLAKE3): +==================+=========================================+ | Component | Size | +==================+=========================================+ | Token size | 193 bytes (1 × 33 bytes + 5 × 32 bytes) | +------------------+-----------------------------------------+ | Spend proof size | 32 × (14 + 4L) + (2 + L) bytes | +------------------+-----------------------------------------+ | Nullifier | 32 bytes per spent token | | database entry | | +------------------+-----------------------------------------+ Table 6 Note: P-256 compressed points are 33 bytes (vs 32 for Ristretto255). Token size is independent of L for both ciphersuites. 6. Security Considerations 6.1. Security Model and Definitions 6.1.1. Threat Model We consider a setting with: * Multiple issuers who can operate independently, though malicious issuers may collude with each other * Potentially malicious clients who may attempt to spend more credits than they should (whether by forging tokens, spending more credits than a token has, or double-spending a token) 6.1.2. Security Properties The protocol provides the following security guarantees: 1. *Unforgeability*: For an honest isser I, no probabilistic polynomial-time (PPT) adversary controlling a set of malicious clients and other malicious issuers can spend more credits than have been issued by I. 2. *Anonymity/Unlinkability*: For an honest client C, no adversary controlling a set of malicious issuers and other malicious clients can link a token issuance/refund to C with a token spend by C. This property is information-theoretic in nature. 6.2. Cryptographic Assumptions Security relies on: 1. *The q-SDH Assumption* in the ciphersuite's group (Ristretto255 or P-256). We refer to [TZ23] for the formal definition. 2. *Random Oracle Model*: The BLAKE3 hash function H is modeled as a random oracle. 6.3. Privacy Properties The protocol provides the following privacy guarantees: 1. *Unlinkability*: The issuer cannot link a token issuance/refund to a later spend of that token. However, the protocol does NOT provide: 1. *Network-Level Privacy*: IP addresses and network metadata can still link transactions. 2. *Amount Privacy*: The spent amount s is revealed to the issuer. 3. *Timing Privacy*: Transaction timing patterns could potentially be used for correlation. 4. *Context Privacy*: The request context (ctx) is revealed in the clear during spending. If the issuer assigns distinct ctx values per issuance, the resulting token chain (issuance, spend, refund, subsequent spends) becomes linkable through the shared ctx value. This is by design for application-level context binding, but deployments that require full unlinkability MUST use a shared ctx across all clients within the same context (e.g., per-service or per-epoch), not per-client values. The ctx value persists across refunds: a token produced by a refund inherits the ctx of the original token. 6.4. Implementation Vulnerabilities and Mitigations 6.4.1. Critical Security Requirements 1. *RNG Failures*: Weak randomness can completely break the protocol's security. *Attack Vector*: Predictable or repeated nonces in proofs can allow complete recovery of secret values including private keys and token contents. *Mitigations*: * MUST use cryptographically secure RNGs (e.g., OS-provided entropy sources) * MUST reseed after fork() operations to prevent nonce reuse * MUST implement forward-secure RNG state management * SHOULD use separate RNG instances for different protocol components * MUST zeroize RNG state on process termination 2. *Timing Attacks*: Variable-time operations can leak information about secret values. *Attack Vector*: Timing variations in scalar arithmetic or bit operations can reveal secret bit patterns, potentially exposing credit balances or allowing token forgery. *Mitigations*: * MUST use constant-time scalar arithmetic libraries * MUST use constant-time conditional selection for range proof conditionals * MUST avoid early-exit conditions based on secret values * Critical constant-time operations include: - Scalar multiplication and addition - Binary decomposition in range proofs - Conditional assignments based on secret bits - Challenge verification comparisons 3. *Nullifier Database Attacks*: Corruption or manipulation of the nullifier database enables double-spending. *Attack Vectors*: * Database corruption allowing nullifier deletion * Race conditions in concurrent nullifier checks *Mitigations*: * MUST use ACID-compliant database transactions * MUST check nullifier uniqueness within the same transaction as insertion * SHOULD implement append-only audit logs for nullifier operations * MUST implement proper database backup and recovery procedures 4. *Eavesdropping/Message Modification Attacks*: A network-level adversary can copy spend proofs or modify messages sent between an honest client and issuer. *Attack Vectors*: * Eavesdropping and copying of proofs * Message modifications causing protocol failure *Mitigations*: * Client and issuer MUST use TLS 1.3 or above when communicating. 5. *State Management Vulnerabilities*: Improper state handling can lead to security breaches. *Attack Vectors*: * State confusion between protocol sessions * Memory disclosure of sensitive state * Incomplete state cleanup *Mitigations*: * MUST use separate state objects for each protocol session * MUST zeroize all sensitive data (keys, nonces, intermediate values) after use * SHOULD use memory protection mechanisms (e.g., mlock) for sensitive data * MUST implement proper error handling that doesn't leak state information * SHOULD use explicit state machines for protocol flow 6. *Concurrency and Race Conditions*: Parallel operations can introduce vulnerabilities. *Attack Vectors*: * TOCTOU (Time-of-check to time-of-use) vulnerabilities in nullifier checking * Race conditions in balance updates * Concurrent modification of shared state *Mitigations*: * MUST use appropriate locking for all shared resources * MUST perform nullifier check and insertion atomically * SHOULD document thread-safety guarantees * MUST ensure atomic read-modify-write for all critical operations 6.5. Known Attack Scenarios 6.5.1. 1. Parallel Spend Attack *Scenario*: A malicious client attempts to spend the same token multiple times by initiating parallel spend operations before any nullifier is recorded. *Prevention*: The issuer MUST ensure atomic nullifier checking and recording within a single database transaction. Network-level rate limiting can provide additional protection. 6.5.2. 2. Balance Inflation Attack *Scenario*: An attacker attempts to create a proof claiming to have more credits than actually issued by manipulating the range proof. *Prevention*: The cryptographic soundness of the range proof prevents this attack. 6.5.3. 3. Token Linking Attack *Scenario*: An issuer attempts to link transactions by analyzing patterns in nullifiers, amounts, or timing. *Prevention*: Nullifiers are cryptographically random and unlinkable. However, implementations MAY add random delays and amount obfuscation where possible. 6.6. Protocol Composition and State Management 6.6.1. State Management Requirements Before they make a spend request or an issue request, the client MUST store their private state (the nullifier, the blinding factor, and the new balance) durably. For the issuer, the spend and refund operations MUST be treated as an atomic transaction. However, even more is required. If a nullifier associated with a given spend is persisted to the database, clients MUST be able to access the associated refund. If they cannot access this, then they can lose access to the rest of their credits. For performance reasons, an issuer SHOULD automatically clean these up after some expiry, but if they do so, they MUST inform the client of this policy so the client can ensure they can retry to retrieve the rest of their credits in time. Issuers MAY implement functionality to notify the issuer that the refund request was processed, so they can delete the refund record. It is not clear that this is worth the cost relative to just cleaning them up in bulk at some specified expiration date, however if you are memory constrained this could be useful. 6.6.2. Session Management Each protocol session (issuance or spend/refund) MUST use fresh randomness. See the Randomness Generation section for detailed RNG requirements. 6.6.3. Version Negotiation To support protocol evolution, implementations MAY include version negotiation in the initial handshake. All parties MUST agree on the protocol version before proceeding. 6.7. Quantum Resistance This protocol is NOT quantum-resistant. The discrete logarithm problem can be solved efficiently by quantum computers using Shor's algorithm. Organizations requiring long-term security should consider post-quantum alternatives. However, user privacy is preserved even in the presence of a cryptographically relevant quantum computer. 7. IANA Considerations This document has no IANA actions. 8. References 8.1. Normative References [BLAKE3] "BLAKE3: One Function, Fast Everywhere", 9 January 2020, . [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . [RFC6090] McGrew, D., Igoe, K., and M. Salter, "Fundamental Elliptic Curve Cryptography Algorithms", RFC 6090, DOI 10.17487/RFC6090, February 2011, . [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . [RFC8949] Bormann, C. and P. Hoffman, "Concise Binary Object Representation (CBOR)", STD 94, RFC 8949, DOI 10.17487/RFC8949, December 2020, . [RFC9380] Faz-Hernandez, A., Scott, S., Sullivan, N., Wahby, R. S., and C. A. Wood, "Hashing to Elliptic Curves", RFC 9380, DOI 10.17487/RFC9380, August 2023, . [RFC9496] de Valence, H., Grigg, J., Hamburg, M., Lovecruft, I., Tankersley, G., and F. Valsorda, "The ristretto255 and decaf448 Groups", RFC 9496, DOI 10.17487/RFC9496, December 2023, . [SEC1] "SEC 1: Elliptic Curve Cryptography", 21 May 2009, . 8.2. Informative References [BBS] "Short Group Signatures", 2004, . [KVAC] "Keyed-Verification Anonymous Credentials", 2014, . [ORRU-FS] "The Fiat-Shamir Transform", 19 January 2025, . [ORRU-SIGMA] "Sigma Protocols", 19 January 2025, . [RFC9474] Denis, F., Jacobs, F., and C. A. Wood, "RSA Blind Signatures", RFC 9474, DOI 10.17487/RFC9474, October 2023, . [TZ23] "Revisiting BBS Signatures", 2023, . Appendix A. Test Vectors This appendix provides test vectors for implementers to verify their implementations. All values are encoded in hexadecimal. A.1. ACT-Ristretto255-BLAKE3 Test Vectors The following test vector was generated deterministically using a ChaCha20 RNG seeded with the bytes 00 01 02 ... 1e 1f and L=8. The domain separator is "ACT-v1:test:vectors:v0:2025-01-01", credit amount c=100, spend amount s=30, partial return t=10, and ctx=0. Values labelled *_cbor are the CBOR wire-format encodings (Section 4) of each protocol message, displayed in hexadecimal. Implementations SHOULD verify they can deserialize these CBOR messages and that a full protocol run with the same deterministic RNG produces identical output. A.2. Parameters domain_separator: "ACT-v1:test:vectors:v0:2025-01-01" L: 8 c: 100 s: 30 t: 10 ctx: 0000000000000000000000000000000000000000000000000000000000000000 A.3. Key Generation sk_cbor: a201582036e5b43419551a92c809a995a3d2c817a86ce8f5dd973b06fe9cb5a3 f012870b0258204aceeb1d507e50957db46b6bcd374614b8ea080cbbc77ad060 666bf5788c8121 pk_cbor: 58204aceeb1d507e50957db46b6bcd374614b8ea080cbbc77ad060666bf5788c 8121 A.4. Issuance preissuance_cbor: a20158206102398efee33b886f4bb7042b897d83db59b71a05aff76e9b633b87 cade7d0002582069e5d557cb6094acfa586118e602e90aa6fe6cbabd4571eeb0 d2f63b8c8a8f07 issuance_request_cbor: a4015820aa9315999f76c89406fe743dc7ff12e8fab85871f8c36987c6ec25ee ca2cd84e025820811880b9160decfbb41006af6c39056c9b0c139f7acf647fb5 b0b22486039504035820c319066c466ef36d08809279c02dac8430c119fae886 7f0c235cd6f6e4514c0f0458206af5dcb3e7138eb2a0f5b523054e05137b558f 1bb6711b10b689e3565c241506 issuance_response_cbor: a60158201eeda51d75404be1bdd06c31aa72bbd38470a4717e732ccf372b91bc 77161a0e0258204778cf09b14bf78e89e5ef5bcb523863d4e70f9d84ae1fbe75 778e60a92c290e035820e94bd324b71702e9f29a239b3a064caa4c46d85d9693 0f75bc39eca6211d2c0f04582029002c4fa8a9f71b8c015fb7869ad64a0fc4e0 50c7c4955ef6ebff27f722890e05582064000000000000000000000000000000 0000000000000000000000000000000006582000000000000000000000000000 00000000000000000000000000000000000000 credit_token_cbor: a60158201eeda51d75404be1bdd06c31aa72bbd38470a4717e732ccf372b91bc 77161a0e0258204778cf09b14bf78e89e5ef5bcb523863d4e70f9d84ae1fbe75 778e60a92c290e03582069e5d557cb6094acfa586118e602e90aa6fe6cbabd45 71eeb0d2f63b8c8a8f070458206102398efee33b886f4bb7042b897d83db59b7 1a05aff76e9b633b87cade7d0005582064000000000000000000000000000000 0000000000000000000000000000000006582000000000000000000000000000 00000000000000000000000000000000000000 A.5. Spending nullifier: 69e5d557cb6094acfa586118e602e90aa6fe6cbabd4571eeb0d2f63b8c8a8f07 context: 0000000000000000000000000000000000000000000000000000000000000000 charge: 1e00000000000000000000000000000000000000000000000000000000000000 spend_proof_cbor: b201582069e5d557cb6094acfa586118e602e90aa6fe6cbabd4571eeb0d2f63b 8c8a8f070258201e000000000000000000000000000000000000000000000000 00000000000000035820b221966bf32eafaac154b44f6037083d01314eee2e46 e11b5003bcd07b6bb074045820562528491b9c19a10cefd74b3e45a89fcfdd62 440997758ae8fa34d1a8d8583d05885820581721b2e63035cfe346849bd92c2b 937b8e9404dabe1e5ca29e77de3321852a5820448491c7d81ecc20a9705b1bdd 4df0c71570df3ad5fd9334758f8df9f56ede32582096431dd3f6ea1d7cb43165 8d6f0fe14795e569b731c3cbe79bf293d78b6674255820cc718e6924e761e85b abf7a2b2336e892bb722c36d5aad88ee962b2d8d6b003a5820349e11dd9143d4 8cf90e62386ebcada9490ab0ebb87739c49863ddd8a984a43a5820cc635618f8 1e27bb91252afb2d66ea443cc17e48d4f08c0041f1071e6ead92145820e6e7cd cde60cf3fcef195aa9c511636eb31ee52ed1a328a821f9600bc997d25d582092 857b094af803da09df1a95db12761e860425b53cb7cd967d3c227e535b991d06 5820c68d2efa21dce1ada7a23b30c34edce8db9d6bc71ce2e8c75921fb179f6c d10607582003918610c7af601b6e22c22d0861e781252a24c6f759c4cda08b8f 1fe7a09003085820c273eff86e662b4d44bcbdb73b0e4eec7714662dd1c0db61 5ad53260a2b7650b0958207dbd42848d4dc3cb0a5461f26a08c3761f5decf00c d97976302a12a1ec46dc0e0a5820e97e15f04be7008f2f1c0bc69d2d4bebfbd1 8bcca213f1ccf6187e933166e9050b5820a19468507a55c3e47628a435e3210e 8024dc8259a06fdf7efb8c0e70d4a07e0d0c5820786081f2c4f6061d3e313f67 cc7f73500283ffefa13a0f829e7ce85c8b19640b0d5820b8b352e7cb2066cef5 cbbb79ae17852b0e44c82dc29b4b9a76d3422037e14c0c0e8858209411a6afca a687cba25c7a3ba2e3f3e12917bce920615a6073b7dceebad92b0b58205d5690 3268f420e1ccc725685a7be6edd935f9a1ddc0e11508d648268e198b09582046 040cd466c38375c1811153671aeed9530a3f052ed30c660d33141252255f0458 209aa8b4018a9a0d53701b704bcd5636b1870749f1fd45dda322764282784707 0a5820e3c62f417f88e5b7ec2182560e0d852e50e37a1d7ac00a8f5e090a7205 f41f0d58205f22918a53fb88546521a44238ce5608c13c6ebf0f11f6784c835a c531b8720a58209d04a1b04cc9f59818dbaf7d04da70d2446145f9534886bce9 5ed2b70df7cd095820346599fabc841424884cd029f3d63ae63158c6b4c975af e0d1147715d462aa020f88825820677b5e013537036d2e0c11ec8117799424bc ee744e7b1bd9571b7c464c7b72085820ba017329a9e86ccbe5ed1c76a1362f39 9724cc7211b77940b6c513155bb1b30e825820ab6c718a173c91b3d30670fa50 862021895ca40c818bc9d15e4110f7c0a4640958203c36da850f85cf51fdc896 54b0a853677308974b9ea38e6e7e49ce94560ca8018258201b7d378abce009fc e50133f2d77380f3492794f72b209407e155f8ed8341bd05582057aa6d5d46a6 d3b63c1d45af5c8d66ebc78494ee94df100b6cefc775fa6db8098258205612d9 c1afb3c81bc81860c99b18466af0797eec6ebb3c6b8207b0521ffdf20c58209f 5184c3bd8bc8d7a3f33941a3e8126850fbc795d4809dfeb5f363a30ff2ee0482 58204c7c14d9881aa291a136ab4979d832cf5c80b75c8fe00ca976f65b3d4c9d 150c5820150b6c20f4b894b4d491a252b4956c72e880639b7880f25f6ad0cba1 6dbd020f8258207b2bc0d260f1f3ac32dfa6643a721a845faf9fe8e5de52751f 5d824fa34e770f58209fe39299e57caa120937957a8957cd6c25f14958ea8e7b 6333a2ac501bc144068258201f19d99a4e9955cc464fe48912f51399358bc678 4a22312a95233e592dd1070558204c666f3adfaa3806b290247faa6950bb74e2 70c9155f5004d407d21f223c0802825820d0b1d956f1b1a4ffb3c1a447bcb22a 0e5679b70ca98357ce0dd5b6670bdc480e582017636a9a14caef700e53785bcb bd98380ac45d53af06107e99911fbf4ccf24001058201c7ad54f635ebd976c1e 4fc275d93ce2e0981bbcaacb3745deda0be61cfe9f07115820c8451b222fdf76 abc8a76dd90d1b5526de7e510809750825eea0cce6a4fe1c0212582000000000 00000000000000000000000000000000000000000000000000000000 prerefund_cbor: a40158200f9288d8ef1360d8ef4967e041bf09a716c093956464370d30dfe283 2be71b06025820ebada4fb4050db92729a58f0ae585f76154103a2ef2166c401 12638f006d280b03582046000000000000000000000000000000000000000000 0000000000000000000004582000000000000000000000000000000000000000 00000000000000000000000000 A.6. Refund refund_cbor: a5015820880974b47fd0d4d06333e2f047abc4420992bd903ed44dae86199a54 361f9c540258208a0977b088e9d17a637f71a013c67774648f0da03b141404ae 678a0e5e090b04035820fdcd645c0d6e13905fff07e56d63465e4cc585f3c247 8500c96cd361a4ad01070458202c9f3110e53540738100e7e636949ce7ac08bf b4ac6867fb72ac6ec847a2f90e0558200a000000000000000000000000000000 00000000000000000000000000000000 A.7. Refund Token refund_token_cbor: a6015820880974b47fd0d4d06333e2f047abc4420992bd903ed44dae86199a54 361f9c540258208a0977b088e9d17a637f71a013c67774648f0da03b141404ae 678a0e5e090b04035820ebada4fb4050db92729a58f0ae585f76154103a2ef21 66c40112638f006d280b0458200f9288d8ef1360d8ef4967e041bf09a716c093 956464370d30dfe2832be71b0605582050000000000000000000000000000000 0000000000000000000000000000000006582000000000000000000000000000 00000000000000000000000000000000000000 refund_token_credits: 5000000000000000000000000000000000000000000000000000000000000000 refund_token_nullifier: ebada4fb4050db92729a58f0ae585f76154103a2ef2166c40112638f006d280b remaining_balance: 80 A.8. ACT-P256-BLAKE3 Test Vectors The following test vector was generated deterministically using a ChaCha20 RNG seeded with the bytes 00 01 02 ... 1e 1f and L=8. The domain separator is "ACT-v1:test:vectors:v0:2025-01-01", credit amount c=100, spend amount s=30, partial return t=10, and ctx=0. Values labelled *_cbor are the CBOR wire-format encodings (Section 4) of each protocol message, displayed in hexadecimal. Implementations SHOULD verify they can deserialize these CBOR messages and that a full protocol run with the same deterministic RNG produces identical output. A.9. Parameters domain_separator: "ACT-v1:test:vectors:v0:2025-01-01" L: 8 c: 100 s: 30 t: 10 ctx: 0000000000000000000000000000000000000000000000000000000000000000 A.10. Key Generation sk_cbor: a201582039fd2b7dd9c5196a8dbd0377b8dc4a498a35d86fbcde6accb2cc7d4c d8ea249202582102ceff7e162d6baee1abd2f72b83fdaa96df661e4375d87561 c8e41936310f3dc4 pk_cbor: 582102ceff7e162d6baee1abd2f72b83fdaa96df661e4375d87561c8e4193631 0f3dc4 A.11. Issuance preissuance_cbor: a20158202b23cce7a26023ab3f0eef693ac87f64258235eab1f7a32dc22762a0 485b410c02582018b84231ade6a6d113615c61af434e27f8b1f3f5e1ad5b5cec f8fc122a35755c issuance_request_cbor: a401582102358340eb58cb3b04194f5e23b83e52ded58642bdd0100ee15436ab 541fa759380258205abde2472a976beab6bfed5391320391b27a6d3e0e27d8da ba5d889744da7978035820803699ba1e0d6e45b96e7055fadb7ea35230e2756a 6bd20761b6d6de79746bc1045820c313acd3c3b92e1b82db8d4cf81d35e88147 b5708520a7c9dc1abd753375b4fd issuance_response_cbor: a6015821031844d93ffac500532e1a1e0c58ac60f7e9a62efc48a1e54d5391fe 620425e4cc02582084a8021b8a5c0b2494cd3c8d5b13507ec7e7a0784df4a3e2 ea8162d261c59d23035820ae9c68c1e0ef77d43f05363862b133e9e19d3e33ef aff74dbcbb847e6ff0798404582048bb20fc0025236d60b3ce05f9ef183ab5ff 919ad487a66329fbf89fc2798939055820000000000000000000000000000000 0000000000000000000000000000000064065820000000000000000000000000 0000000000000000000000000000000000000000 credit_token_cbor: a6015821031844d93ffac500532e1a1e0c58ac60f7e9a62efc48a1e54d5391fe 620425e4cc02582084a8021b8a5c0b2494cd3c8d5b13507ec7e7a0784df4a3e2 ea8162d261c59d2303582018b84231ade6a6d113615c61af434e27f8b1f3f5e1 ad5b5cecf8fc122a35755c0458202b23cce7a26023ab3f0eef693ac87f642582 35eab1f7a32dc22762a0485b410c055820000000000000000000000000000000 0000000000000000000000000000000064065820000000000000000000000000 0000000000000000000000000000000000000000 A.12. Spending nullifier: 18b84231ade6a6d113615c61af434e27f8b1f3f5e1ad5b5cecf8fc122a35755c context: 0000000000000000000000000000000000000000000000000000000000000000 charge: 000000000000000000000000000000000000000000000000000000000000001e spend_proof_cbor: b201582018b84231ade6a6d113615c61af434e27f8b1f3f5e1ad5b5cecf8fc12 2a35755c02582000000000000000000000000000000000000000000000000000 0000000000001e035821027f7b287b0596827ed15233fe7eebde70203c2a9cb4 e95d9df4275223f1cd38f704582102ff193690e1383a109ddc0cc7e2779ba7d6 8cceca05a58d33e51f493c8ea023f90588582103c9f801fac5f9322e5201eb11 64af6d3465086a2a71e4fc3846c612a1efdd6cd2582103c97c553fd03cd4e25c c182c93445c9844f4c31516efeb9728a63c5633d75d205582103fd9b1e3f9a21 0d944940102ca89109f381e721c9be6f26b9f03c1dfbf09dea7d582103f40c70 b7090ea29e4e0f60d71c5e904bcb259718d2b689005cfd812aede1e3a0582102 7ee378f75bda9c22c26a90c7b02728aed85a380bef2bf61cb8239c40aebbbeaf 582102d9c87217d2861afcd64e059a40e1c9f978d236b881ea82ed813016aa03 cd78a6582103274e1d1c77c853eaa4bc3142b4548c18f2ea597fa803177d2b14 8541b89f67a75821039fcf8e9ff9629b0e97c6dad939ef8511e8895982095aa1 cd130570260f946423065820a59029ad3ebcb5b102a54edccd1113c871538110 e70fdc2eaf81c4518551ee090758208b90854c5b523b0570c7ed241edbea738b fa6b3ed8088fb5bd76d4111e0332b008582064d847cec8cfdbd0ca915bdb43a3 255d7560c7b5e9a060aab2d2660e441b99280958208919406d99a15b2f3632aa b30185b9caa1aec1d94dbb9b644478c3d3cfa568900a582035dd0b422bc0075b f2508616409e4995131ad4e9c10515f559602178ea68ae210b5820fd3c6d5c74 be7882da2ea3868e6235fd4cc729ee96693a955faef310bd819c220c58203a35 be24f1e9fcc736f37446c992305169ed44a244047028976d312014ce46030d58 206a9192e22167b171783edc442d1debea35c8c62cd27be25ccdd0cb887fc41b 530e8858206f75eddbe879e025f7947480fe74c087119021265d02cc9c7129e1 ed01b986105820ceca4aa2ff6d7aae8d4a8e9ad50786663932729106f437bfb4 f735df0b52b40a58204fa9a0fbc0552298605991cea5c0600a388e83f6d065e2 aae3666aa6e87d69e6582088f9208c8963bef8c8f9e708ec5c2853ba0aa700b3 4c787f5f14547aa79ab1125820fce7591b1655bf87dea9d2275df6166465ab4f 4f608bef944a1911dfcc69704358200447145f159c7360f0d6c2e07b62f1149c 30af49b4da1a51f8d659bcfde6532e58209ca80e6252fae8f0bf5bbbf33157a0 fda42ed9f25cc3fe099a2b1fbd793e1c345820bf1dea7ff1e7ef28623d97aed5 0f602044fb14e217f455464a1e34a35fa28e450f888258206633bd5f3bc3e36a 4478ffc44908e40896600601a771c9bd649fd87918118389582021dfcb29a804 6791dca97da2e977616065171fdca134c9e02e67fe356c70da208258207d7a20 ccff17e7873f7f3465f6c8c70abc53d97ff7dac95f56a19a103edcd9a2582055 86bf9fffd12a4fb460d27da82fa5634008857125ea3b57b6bd3df5be1af1b282 5820d45cab4cd1d9924b4bbfbaf09c34769621184923e61d8384a064d607beaa 72b458208c9007089c63b6da696874b5921b449cdabad5fa6c02bb52fbdf23f6 8ad3031f8258209a711f5fbfbf95a6797f0fb3a39639ba911f31c500aa1e6856 b474490ace8542582005d644e79d25d9b4b913ad9a64826a5dd60edfa8bbf1c7 604c77c81b8d42853582582032e5cdf6b1eba913b48729b9ff763cb24ac2bd5c a2b933aae20ffbcbac1c61cc5820183588ca784a4e5606a2627b641ca044376b dfb7f2b3e3c8f5bc5dc1c6ff0305825820cb1793e3734fe20c4591942e804cad 1828388c56087515027612874c6b28e75e5820bb6e37774d548b6d6a714bd84b fdfbc94074836079640e86d6998877c9716a59825820d44d65d01b23b9c3789e 75b5b5a34751f269d2d81d80bc7d33d2e41a71b242415820e9cd1663b32ad7e3 ce9e15877c0ae4f227d1007c9b1191c5cba63d1fd24679cb825820bd1507cfb8 86b58a19e51a39eb6ad5726350b335e1a0d1e4f61f9aff92acb73e5820627d76 64c5fd194f8f1b38539167d77bcc036beed2f47005e69a940ccf52b239105820 17587f83019424a00e41e603749042df83e9a03e55d9f3f7121993795b7b083b 115820ffd21e5992960d6f726ce89d350be9a042df7a644c1c85ce2c11df4bf9 6625f81258200000000000000000000000000000000000000000000000000000 000000000000 prerefund_cbor: a4015820a7173d05ed1dcb97392f4e8da31fbf2fae2856706441f486418dffd1 7794fefd025820185838beabf85b1605467c46149350e877815eefc73f7d9b3d 94b198d7fef9c903582000000000000000000000000000000000000000000000 0000000000000000004604582000000000000000000000000000000000000000 00000000000000000000000000 A.13. Refund refund_cbor: a501582102c184cdca231ce8e9ac3097f1d144b2750167967a20781158d4d28d fe658cb381025820608f22735090d5bc4c5008960d409643e95e2fbbac5573d3 8961045c80dc83fb035820e4a691aadf08b37e4f23786bc23119d47456fc80ce 754caca9ae96960341fc4d04582025fd1cf8008bc06d3598355a754abf0d680d f295ddf46232a812e427fe0f7462055820000000000000000000000000000000 000000000000000000000000000000000a A.14. Refund Token refund_token_cbor: a601582102c184cdca231ce8e9ac3097f1d144b2750167967a20781158d4d28d fe658cb381025820608f22735090d5bc4c5008960d409643e95e2fbbac5573d3 8961045c80dc83fb035820185838beabf85b1605467c46149350e877815eefc7 3f7d9b3d94b198d7fef9c9045820a7173d05ed1dcb97392f4e8da31fbf2fae28 56706441f486418dffd17794fefd055820000000000000000000000000000000 0000000000000000000000000000000050065820000000000000000000000000 0000000000000000000000000000000000000000 refund_token_credits: 0000000000000000000000000000000000000000000000000000000000000050 refund_token_nullifier: 185838beabf85b1605467c46149350e877815eefc73f7d9b3d94b198d7fef9c9 remaining_balance: 80 Appendix B. Implementation Status This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in RFC 7942. B.1. anonymous-credit-tokens Organization: Google Description: Reference implementation in Rust Maturity: Beta Coverage: Complete protocol implementation License: Apache 2.0 Contact: sgschlesinger@gmail.com URL: https://github.com/SamuelSchlesinger/anonymous-credit-tokens Appendix C. Terminology Glossary This glossary provides quick definitions of key terms used throughout this document: *ACT (Anonymous Credit Tokens)*: The privacy-preserving authentication protocol specified in this document. *Blind Signature*: A cryptographic signature where the signer signs a message without seeing its content. *Refund*: The refund issued for the remaining balance after a partial spend. *Credit*: A numerical unit of authorization that can be spent by clients. *Domain Separator*: A unique string used to ensure cryptographic isolation between different deployments. *Element*: A point in the ciphersuite's prime-order elliptic curve group (Ristretto255 or P-256). *Issuer*: The entity that creates and signs credit tokens. *Nullifier*: A unique value revealed during spending that prevents double-spending of the same token. *Partial Spending*: The ability to spend less than the full value of a token and receive change. *Scalar*: An integer modulo the group order q, used in cryptographic operations. *Sigma Protocol*: An interactive zero-knowledge proof protocol following a commit-challenge-response pattern. *Token*: A cryptographic credential containing a BBS signature and associated data (A, e, k, r, c, ctx). *Unlinkability*: The property that transactions cannot be correlated with each other or with token issuance. Appendix D. Acknowledgments The authors would like to thank the Crypto Forum Research Group for their valuable feedback and suggestions. Special thanks to the contributors who provided implementation guidance and security analysis. This work builds upon the foundational research in anonymous credentials and zero-knowledge proofs by numerous researchers in the cryptographic community, particularly the work on BBS signatures by Boneh, Boyen, and Shacham, and keyed-verification anonymous credentials by Chase, Meiklejohn, and Zaverucha. Authors' Addresses Samuel Schlesinger Google Email: samschlesinger@google.com Jonathan Katz Google Email: jkcrypto@google.com