@@ -43,6 +43,51 @@ function isValidScalarHex(hex: string): boolean {
4343 return HEX_SCALAR_64 . test ( hex ) ;
4444}
4545
46+ function normalizeBindingContext ( bindingContext ?: string ) : string | undefined {
47+ return bindingContext === '' ? undefined : bindingContext ;
48+ }
49+
50+ function bindingContextToBytes ( bindingContext ?: string ) : Uint8Array {
51+ const normalized = normalizeBindingContext ( bindingContext ) ;
52+ if ( normalized === undefined ) return EMPTY_CONTEXT ;
53+ if ( normalized . length > MAX_CONTEXT_BYTES ) {
54+ throw new ValidationError ( 'Binding context exceeds maximum length (1024 bytes)' ) ;
55+ }
56+ const context = utf8ToBytes ( normalized ) ;
57+ if ( context . length > MAX_CONTEXT_BYTES ) {
58+ throw new ValidationError ( 'Binding context exceeds maximum length (1024 bytes)' ) ;
59+ }
60+ return context ;
61+ }
62+
63+ function sameBindingContext ( left ?: string , right ?: string ) : boolean {
64+ return normalizeBindingContext ( left ) === normalizeBindingContext ( right ) ;
65+ }
66+
67+ function parseAgeRange ( ageRange : string ) : { min : number ; max : number } {
68+ const DIGITS_ONLY = / ^ \d + $ / ;
69+
70+ if ( ageRange . endsWith ( '+' ) ) {
71+ const minStr = ageRange . slice ( 0 , - 1 ) ;
72+ if ( ! DIGITS_ONLY . test ( minStr ) ) {
73+ throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
74+ }
75+ return { min : parseInt ( minStr , 10 ) , max : 150 } ;
76+ }
77+
78+ const parts = ageRange . split ( '-' ) ;
79+ if ( parts . length !== 2 ) {
80+ throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
81+ }
82+ if ( ! DIGITS_ONLY . test ( parts [ 0 ] ) || ! DIGITS_ONLY . test ( parts [ 1 ] ) ) {
83+ throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
84+ }
85+ return {
86+ min : parseInt ( parts [ 0 ] , 10 ) ,
87+ max : parseInt ( parts [ 1 ] , 10 ) ,
88+ } ;
89+ }
90+
4691// --- Pedersen Commitment ---
4792
4893export interface PedersenCommitment {
@@ -387,11 +432,8 @@ export function createRangeProof(value: number, min: number, max: number, bindin
387432 if ( max < min ) throw new ValidationError ( 'Maximum must be >= minimum' ) ;
388433 if ( value < min || value > max ) throw new ValidationError ( 'Value is not within the specified range' ) ;
389434
390- if ( bindingContext !== undefined && bindingContext . length > MAX_CONTEXT_BYTES ) {
391- throw new ValidationError ( 'Binding context exceeds maximum length (1024 bytes)' ) ;
392- }
393- const context = bindingContext ? utf8ToBytes ( bindingContext ) : EMPTY_CONTEXT ;
394- if ( context . length > MAX_CONTEXT_BYTES ) throw new ValidationError ( 'Binding context exceeds maximum length (1024 bytes)' ) ;
435+ const normalizedBindingContext = normalizeBindingContext ( bindingContext ) ;
436+ const context = bindingContextToBytes ( normalizedBindingContext ) ;
395437
396438 const range = max - min ;
397439 const bits = bitsNeeded ( range ) ;
@@ -462,26 +504,38 @@ export function createRangeProof(value: number, min: number, max: number, bindin
462504 sumBindingS : sumBinding . s ,
463505 commitBindingE : commitBinding . e ,
464506 commitBindingS : commitBinding . s ,
465- context : bindingContext ,
507+ ... ( normalizedBindingContext !== undefined ? { context : normalizedBindingContext } : { } ) ,
466508 } ;
467509}
468510
469511/**
470512 * Verify a range proof.
471513 *
472514 * @param proof - The range proof to verify
515+ * @param expectedMin - The minimum bound the verifier expects
516+ * @param expectedMax - The maximum bound the verifier expects
517+ * @param expectedBindingContext - Optional binding context the verifier expects
473518 * @returns true if the proof is valid
474519 */
475- export function verifyRangeProof ( proof : RangeProof ) : boolean {
520+ export function verifyRangeProof (
521+ proof : RangeProof ,
522+ expectedMin : number ,
523+ expectedMax : number ,
524+ expectedBindingContext ?: string
525+ ) : boolean {
476526 try {
477527 const { min, max, bits, lowerProof, upperProof } = proof ;
478- if ( proof . context && proof . context . length > MAX_CONTEXT_BYTES ) return false ;
479- const context = proof . context ? utf8ToBytes ( proof . context ) : EMPTY_CONTEXT ;
480- if ( context . length > MAX_CONTEXT_BYTES ) return false ;
528+ if ( ! Number . isSafeInteger ( expectedMin ) || ! Number . isSafeInteger ( expectedMax ) ) return false ;
529+ if ( expectedMin < 0 || expectedMax < 0 || expectedMax < expectedMin ) return false ;
530+
531+ const context = bindingContextToBytes ( proof . context ) ;
532+ bindingContextToBytes ( expectedBindingContext ) ;
481533
482534 // Validate range bounds
483535 if ( ! Number . isSafeInteger ( min ) || ! Number . isSafeInteger ( max ) ) return false ;
484536 if ( min < 0 || max < 0 || max < min ) return false ;
537+ if ( min !== expectedMin || max !== expectedMax ) return false ;
538+ if ( ! sameBindingContext ( proof . context , expectedBindingContext ) ) return false ;
485539
486540 // Recompute expected bits from range — do not trust proof.bits blindly
487541 const expectedBits = bitsNeeded ( max - min ) ;
@@ -557,31 +611,21 @@ export function verifyRangeProof(proof: RangeProof): boolean {
557611 * @returns The range proof
558612 */
559613export function createAgeRangeProof ( age : number , ageRange : string , subjectPubkey ?: string ) : RangeProof {
560- const DIGITS_ONLY = / ^ \d + $ / ;
561-
562- // Handle "18+" format (no upper bound — use 150 as practical max)
563- if ( ageRange . endsWith ( '+' ) ) {
564- const minStr = ageRange . slice ( 0 , - 1 ) ;
565- if ( ! DIGITS_ONLY . test ( minStr ) ) throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
566- return createRangeProof ( age , parseInt ( minStr , 10 ) , 150 , subjectPubkey ) ;
567- }
568-
569- const parts = ageRange . split ( '-' ) ;
570- if ( parts . length !== 2 ) throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
571- if ( ! DIGITS_ONLY . test ( parts [ 0 ] ) || ! DIGITS_ONLY . test ( parts [ 1 ] ) ) {
572- throw new ValidationError ( 'Invalid age range format (expected "min-max" or "min+")' ) ;
573- }
574- const min = parseInt ( parts [ 0 ] , 10 ) ;
575- const max = parseInt ( parts [ 1 ] , 10 ) ;
614+ const { min, max } = parseAgeRange ( ageRange ) ;
576615
577616 return createRangeProof ( age , min , max , subjectPubkey ) ;
578617}
579618
580619/**
581620 * Verify an age range proof.
582621 */
583- export function verifyAgeRangeProof ( proof : RangeProof ) : boolean {
584- return verifyRangeProof ( proof ) ;
622+ export function verifyAgeRangeProof ( proof : RangeProof , expectedAgeRange : string , expectedSubjectPubkey ?: string ) : boolean {
623+ try {
624+ const { min, max } = parseAgeRange ( expectedAgeRange ) ;
625+ return verifyRangeProof ( proof , min , max , expectedSubjectPubkey ) ;
626+ } catch {
627+ return false ;
628+ }
585629}
586630
587631/**
0 commit comments