@sip-protocol/sdk 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.mts +1 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +2926 -341
- package/dist/browser.mjs +48 -2
- package/dist/chunk-2XIVXWHA.mjs +1930 -0
- package/dist/chunk-3M3HNQCW.mjs +18253 -0
- package/dist/chunk-7RFRWDCW.mjs +1504 -0
- package/dist/chunk-F6F73W35.mjs +16166 -0
- package/dist/chunk-OFDBEIEK.mjs +16166 -0
- package/dist/chunk-SF7YSLF5.mjs +1515 -0
- package/dist/chunk-WWUSGOXE.mjs +17129 -0
- package/dist/index-8MQz13eJ.d.mts +13746 -0
- package/dist/index-B71aXVzk.d.ts +13264 -0
- package/dist/index-DIBZHOOQ.d.ts +13746 -0
- package/dist/index-pOIIuwfV.d.mts +13264 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2911 -326
- package/dist/index.mjs +48 -2
- package/dist/solana-4O4K45VU.mjs +46 -0
- package/dist/solana-NDABAZ6P.mjs +56 -0
- package/dist/solana-ZYO63LY5.mjs +46 -0
- package/package.json +3 -3
- package/src/chains/solana/index.ts +24 -0
- package/src/chains/solana/providers/generic.ts +160 -0
- package/src/chains/solana/providers/helius.ts +249 -0
- package/src/chains/solana/providers/index.ts +54 -0
- package/src/chains/solana/providers/interface.ts +178 -0
- package/src/chains/solana/providers/webhook.ts +519 -0
- package/src/chains/solana/scan.ts +88 -8
- package/src/chains/solana/types.ts +20 -1
- package/src/compliance/index.ts +14 -0
- package/src/compliance/range-sas.ts +591 -0
- package/src/index.ts +99 -0
- package/src/privacy-backends/index.ts +86 -0
- package/src/privacy-backends/interface.ts +263 -0
- package/src/privacy-backends/privacycash-types.ts +278 -0
- package/src/privacy-backends/privacycash.ts +460 -0
- package/src/privacy-backends/registry.ts +278 -0
- package/src/privacy-backends/router.ts +346 -0
- package/src/privacy-backends/sip-native.ts +253 -0
- package/src/proofs/noir.ts +1 -1
- package/src/surveillance/algorithms/address-reuse.ts +143 -0
- package/src/surveillance/algorithms/cluster.ts +247 -0
- package/src/surveillance/algorithms/exchange.ts +295 -0
- package/src/surveillance/algorithms/temporal.ts +337 -0
- package/src/surveillance/analyzer.ts +442 -0
- package/src/surveillance/index.ts +64 -0
- package/src/surveillance/scoring.ts +372 -0
- package/src/surveillance/types.ts +264 -0
package/src/compliance/index.ts
CHANGED
|
@@ -83,3 +83,17 @@ export {
|
|
|
83
83
|
type DeriveViewingKeyParams,
|
|
84
84
|
type DeriveMultipleParams,
|
|
85
85
|
} from './derivation'
|
|
86
|
+
|
|
87
|
+
// Range SAS (Solana Attestation Service) Integration
|
|
88
|
+
export {
|
|
89
|
+
AttestationGatedDisclosure,
|
|
90
|
+
AttestationSchema,
|
|
91
|
+
createMockAttestation,
|
|
92
|
+
verifyAttestationSignature,
|
|
93
|
+
fetchAttestation,
|
|
94
|
+
type RangeSASAttestation,
|
|
95
|
+
type AttestationGatedConfig,
|
|
96
|
+
type ViewingKeyDerivationResult,
|
|
97
|
+
type ViewingKeyScope,
|
|
98
|
+
type AttestationVerificationResult,
|
|
99
|
+
} from './range-sas'
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range SAS (Solana Attestation Service) Integration
|
|
3
|
+
*
|
|
4
|
+
* Enables attestation-gated viewing key disclosure for regulatory compliance.
|
|
5
|
+
* Auditors must present a valid SAS attestation to receive viewing keys.
|
|
6
|
+
*
|
|
7
|
+
* ## How It Works
|
|
8
|
+
*
|
|
9
|
+
* 1. Auditor obtains a KYC/compliance attestation from Range SAS
|
|
10
|
+
* 2. Auditor presents attestation to the viewing key holder
|
|
11
|
+
* 3. System verifies attestation on-chain or via Range API
|
|
12
|
+
* 4. If valid, a scoped viewing key is derived for the auditor
|
|
13
|
+
* 5. Auditor can now decrypt and view transaction history
|
|
14
|
+
*
|
|
15
|
+
* ## Security Properties
|
|
16
|
+
*
|
|
17
|
+
* - **Attestation-gated**: Only verified auditors receive keys
|
|
18
|
+
* - **Scoped access**: Derived keys can be time-limited or scope-limited
|
|
19
|
+
* - **Non-transferable**: Keys are bound to auditor's attestation
|
|
20
|
+
* - **Revocable**: Revoking attestation invalidates the viewing key
|
|
21
|
+
*
|
|
22
|
+
* @see https://www.range.org/blog/introducing-solana-attestation-service
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { AttestationGatedDisclosure, RangeSASAttestation } from '@sip-protocol/sdk'
|
|
27
|
+
*
|
|
28
|
+
* // Create disclosure manager with organization's master viewing key
|
|
29
|
+
* const disclosure = new AttestationGatedDisclosure({
|
|
30
|
+
* masterViewingKey: organizationViewingKey,
|
|
31
|
+
* allowedSchemas: ['range-kyc-v1', 'range-accredited-investor'],
|
|
32
|
+
* })
|
|
33
|
+
*
|
|
34
|
+
* // Auditor presents their attestation
|
|
35
|
+
* const attestation: RangeSASAttestation = {
|
|
36
|
+
* uid: 'sas_123...',
|
|
37
|
+
* schema: 'range-kyc-v1',
|
|
38
|
+
* issuer: 'range-protocol',
|
|
39
|
+
* subject: 'auditor-wallet-address',
|
|
40
|
+
* data: { level: 'institutional', jurisdiction: 'US' },
|
|
41
|
+
* timestamp: Date.now() / 1000,
|
|
42
|
+
* expiresAt: Date.now() / 1000 + 365 * 24 * 60 * 60,
|
|
43
|
+
* signature: '0x...',
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // Verify and derive viewing key
|
|
47
|
+
* const result = await disclosure.deriveViewingKeyForAuditor(attestation)
|
|
48
|
+
* if (result.granted) {
|
|
49
|
+
* console.log('Auditor viewing key:', result.viewingKey)
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { ViewingKey, HexString, Hash } from '@sip-protocol/types'
|
|
55
|
+
import { sha256 } from '@noble/hashes/sha256'
|
|
56
|
+
import { hmac } from '@noble/hashes/hmac'
|
|
57
|
+
import { sha512 } from '@noble/hashes/sha512'
|
|
58
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'
|
|
59
|
+
import { ValidationError, ErrorCode } from '../errors'
|
|
60
|
+
import { secureWipe } from '../secure-memory'
|
|
61
|
+
|
|
62
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Range SAS attestation structure
|
|
66
|
+
*
|
|
67
|
+
* Represents a verifiable claim issued by Range SAS.
|
|
68
|
+
*/
|
|
69
|
+
export interface RangeSASAttestation {
|
|
70
|
+
/** Unique identifier for this attestation */
|
|
71
|
+
uid: string
|
|
72
|
+
/** Schema defining the attestation type (e.g., 'range-kyc-v1') */
|
|
73
|
+
schema: string
|
|
74
|
+
/** Address of the attestation issuer */
|
|
75
|
+
issuer: string
|
|
76
|
+
/** Address of the attestation subject (auditor wallet) */
|
|
77
|
+
subject: string
|
|
78
|
+
/** Attestation data payload */
|
|
79
|
+
data: Record<string, unknown>
|
|
80
|
+
/** Unix timestamp when attestation was created */
|
|
81
|
+
timestamp: number
|
|
82
|
+
/** Unix timestamp when attestation expires (0 = never) */
|
|
83
|
+
expiresAt: number
|
|
84
|
+
/** Cryptographic signature from issuer */
|
|
85
|
+
signature: string
|
|
86
|
+
/** Whether the attestation has been revoked */
|
|
87
|
+
revoked?: boolean
|
|
88
|
+
/** On-chain transaction signature (if stored on-chain) */
|
|
89
|
+
txSignature?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Supported attestation schemas
|
|
94
|
+
*/
|
|
95
|
+
export enum AttestationSchema {
|
|
96
|
+
/** Basic KYC verification */
|
|
97
|
+
RANGE_KYC_V1 = 'range-kyc-v1',
|
|
98
|
+
/** Accredited investor status */
|
|
99
|
+
RANGE_ACCREDITED_INVESTOR = 'range-accredited-investor',
|
|
100
|
+
/** Institutional entity verification */
|
|
101
|
+
RANGE_INSTITUTIONAL = 'range-institutional',
|
|
102
|
+
/** Regulatory authority attestation */
|
|
103
|
+
RANGE_REGULATOR = 'range-regulator',
|
|
104
|
+
/** Custom schema (requires explicit approval) */
|
|
105
|
+
CUSTOM = 'custom',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Configuration for attestation-gated disclosure
|
|
110
|
+
*/
|
|
111
|
+
export interface AttestationGatedConfig {
|
|
112
|
+
/** Master viewing key to derive auditor keys from */
|
|
113
|
+
masterViewingKey: ViewingKey
|
|
114
|
+
/** Allowed attestation schemas (empty = all schemas) */
|
|
115
|
+
allowedSchemas?: string[]
|
|
116
|
+
/** Allowed issuers (empty = all issuers) */
|
|
117
|
+
allowedIssuers?: string[]
|
|
118
|
+
/** Whether to verify attestations on-chain (default: false = API verification) */
|
|
119
|
+
verifyOnChain?: boolean
|
|
120
|
+
/** Range API endpoint for verification */
|
|
121
|
+
rangeApiEndpoint?: string
|
|
122
|
+
/** Minimum attestation age in seconds (prevents replay attacks) */
|
|
123
|
+
minAttestationAge?: number
|
|
124
|
+
/** Maximum attestation age in seconds (enforces time-bounded access, 0 = no limit) */
|
|
125
|
+
maxAttestationAge?: number
|
|
126
|
+
/** Maximum number of cached derived keys (default: 1000) */
|
|
127
|
+
maxCacheSize?: number
|
|
128
|
+
/** Custom verification function */
|
|
129
|
+
customVerifier?: (attestation: RangeSASAttestation) => Promise<boolean>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Result of viewing key derivation
|
|
134
|
+
*/
|
|
135
|
+
export interface ViewingKeyDerivationResult {
|
|
136
|
+
/** Whether access was granted */
|
|
137
|
+
granted: boolean
|
|
138
|
+
/** The derived viewing key (if granted) */
|
|
139
|
+
viewingKey?: ViewingKey
|
|
140
|
+
/** Reason for denial (if not granted) */
|
|
141
|
+
reason?: string
|
|
142
|
+
/** Scope of the granted access */
|
|
143
|
+
scope?: ViewingKeyScope
|
|
144
|
+
/** Expiration timestamp of the viewing key */
|
|
145
|
+
expiresAt?: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Scope of viewing key access
|
|
150
|
+
*/
|
|
151
|
+
export interface ViewingKeyScope {
|
|
152
|
+
/** Start timestamp for viewable transactions */
|
|
153
|
+
startTime?: number
|
|
154
|
+
/** End timestamp for viewable transactions */
|
|
155
|
+
endTime?: number
|
|
156
|
+
/** Specific transaction types viewable */
|
|
157
|
+
transactionTypes?: string[]
|
|
158
|
+
/** Maximum number of transactions viewable */
|
|
159
|
+
maxTransactions?: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Attestation verification result
|
|
164
|
+
*/
|
|
165
|
+
export interface AttestationVerificationResult {
|
|
166
|
+
/** Whether the attestation is valid */
|
|
167
|
+
valid: boolean
|
|
168
|
+
/** Verification errors (if any) */
|
|
169
|
+
errors: string[]
|
|
170
|
+
/** Attestation metadata */
|
|
171
|
+
metadata?: {
|
|
172
|
+
issuerName?: string
|
|
173
|
+
schemaVersion?: string
|
|
174
|
+
verificationMethod: 'on-chain' | 'api' | 'custom'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Attestation-Gated Disclosure ─────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Attestation-gated viewing key disclosure
|
|
182
|
+
*
|
|
183
|
+
* Manages the secure disclosure of viewing keys to verified auditors.
|
|
184
|
+
* Only auditors with valid Range SAS attestations can receive keys.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Default maximum cache size for derived keys
|
|
188
|
+
*/
|
|
189
|
+
const DEFAULT_MAX_CACHE_SIZE = 1000
|
|
190
|
+
|
|
191
|
+
export class AttestationGatedDisclosure {
|
|
192
|
+
private readonly config: Required<AttestationGatedConfig>
|
|
193
|
+
private readonly derivedKeys: Map<string, ViewingKey> = new Map()
|
|
194
|
+
private readonly cacheOrder: string[] = [] // LRU tracking
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Create a new attestation-gated disclosure manager
|
|
198
|
+
*
|
|
199
|
+
* @param config - Configuration options
|
|
200
|
+
*/
|
|
201
|
+
constructor(config: AttestationGatedConfig) {
|
|
202
|
+
if (!config.masterViewingKey) {
|
|
203
|
+
throw new ValidationError(
|
|
204
|
+
'masterViewingKey is required',
|
|
205
|
+
'masterViewingKey',
|
|
206
|
+
undefined,
|
|
207
|
+
ErrorCode.MISSING_REQUIRED
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.config = {
|
|
212
|
+
masterViewingKey: config.masterViewingKey,
|
|
213
|
+
allowedSchemas: config.allowedSchemas ?? [],
|
|
214
|
+
allowedIssuers: config.allowedIssuers ?? [],
|
|
215
|
+
verifyOnChain: config.verifyOnChain ?? false,
|
|
216
|
+
rangeApiEndpoint: config.rangeApiEndpoint ?? 'https://api.range.org/v1',
|
|
217
|
+
minAttestationAge: config.minAttestationAge ?? 0,
|
|
218
|
+
maxAttestationAge: config.maxAttestationAge ?? 0, // 0 = no limit
|
|
219
|
+
maxCacheSize: config.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE,
|
|
220
|
+
customVerifier: config.customVerifier ?? (async () => true),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Derive a viewing key for a verified auditor
|
|
226
|
+
*
|
|
227
|
+
* @param attestation - The auditor's Range SAS attestation
|
|
228
|
+
* @param scope - Optional scope restrictions for the viewing key
|
|
229
|
+
* @returns Derivation result with viewing key if granted
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const result = await disclosure.deriveViewingKeyForAuditor(attestation, {
|
|
234
|
+
* startTime: Date.now() / 1000 - 30 * 24 * 60 * 60, // Last 30 days
|
|
235
|
+
* endTime: Date.now() / 1000,
|
|
236
|
+
* })
|
|
237
|
+
*
|
|
238
|
+
* if (result.granted) {
|
|
239
|
+
* // Share result.viewingKey with auditor
|
|
240
|
+
* }
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
async deriveViewingKeyForAuditor(
|
|
244
|
+
attestation: RangeSASAttestation,
|
|
245
|
+
scope?: ViewingKeyScope
|
|
246
|
+
): Promise<ViewingKeyDerivationResult> {
|
|
247
|
+
// Step 1: Verify the attestation
|
|
248
|
+
const verification = await this.verifyAttestation(attestation)
|
|
249
|
+
|
|
250
|
+
if (!verification.valid) {
|
|
251
|
+
return {
|
|
252
|
+
granted: false,
|
|
253
|
+
reason: verification.errors.join('; '),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 2: Check if we've already derived a key for this attestation
|
|
258
|
+
const cacheKey = this.getCacheKey(attestation)
|
|
259
|
+
const cached = this.derivedKeys.get(cacheKey)
|
|
260
|
+
if (cached) {
|
|
261
|
+
// Update LRU order
|
|
262
|
+
this.updateCacheOrder(cacheKey)
|
|
263
|
+
return {
|
|
264
|
+
granted: true,
|
|
265
|
+
viewingKey: cached,
|
|
266
|
+
scope,
|
|
267
|
+
expiresAt: attestation.expiresAt, // 0 = never expires, undefined = not set
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Step 3: Derive a unique viewing key for this auditor
|
|
272
|
+
const viewingKey = this.deriveKeyFromAttestation(attestation)
|
|
273
|
+
|
|
274
|
+
// Step 4: Cache the derived key with LRU eviction
|
|
275
|
+
this.cacheKey(cacheKey, viewingKey)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
granted: true,
|
|
279
|
+
viewingKey,
|
|
280
|
+
scope,
|
|
281
|
+
expiresAt: attestation.expiresAt, // 0 = never expires, undefined = not set
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Verify a Range SAS attestation
|
|
287
|
+
*
|
|
288
|
+
* @param attestation - The attestation to verify
|
|
289
|
+
* @returns Verification result
|
|
290
|
+
*/
|
|
291
|
+
async verifyAttestation(
|
|
292
|
+
attestation: RangeSASAttestation
|
|
293
|
+
): Promise<AttestationVerificationResult> {
|
|
294
|
+
const errors: string[] = []
|
|
295
|
+
|
|
296
|
+
// Validate required fields exist and are non-empty
|
|
297
|
+
if (!attestation || typeof attestation !== 'object') {
|
|
298
|
+
return {
|
|
299
|
+
valid: false,
|
|
300
|
+
errors: ['Attestation must be an object'],
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!attestation.uid || typeof attestation.uid !== 'string' || attestation.uid.trim() === '') {
|
|
305
|
+
errors.push('Attestation uid is required and must be a non-empty string')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!attestation.subject || typeof attestation.subject !== 'string' || attestation.subject.trim() === '') {
|
|
309
|
+
errors.push('Attestation subject is required and must be a non-empty string')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!attestation.schema || typeof attestation.schema !== 'string' || attestation.schema.trim() === '') {
|
|
313
|
+
errors.push('Attestation schema is required and must be a non-empty string')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!attestation.issuer || typeof attestation.issuer !== 'string' || attestation.issuer.trim() === '') {
|
|
317
|
+
errors.push('Attestation issuer is required and must be a non-empty string')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// If basic validation fails, return early
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
return { valid: false, errors }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check if attestation is revoked
|
|
326
|
+
if (attestation.revoked) {
|
|
327
|
+
errors.push('Attestation has been revoked')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check expiration
|
|
331
|
+
const now = Date.now() / 1000
|
|
332
|
+
if (attestation.expiresAt > 0 && attestation.expiresAt < now) {
|
|
333
|
+
errors.push('Attestation has expired')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check minimum age (anti-replay)
|
|
337
|
+
const age = now - attestation.timestamp
|
|
338
|
+
if (age < this.config.minAttestationAge) {
|
|
339
|
+
errors.push(`Attestation too new (age: ${age}s, required: ${this.config.minAttestationAge}s)`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check maximum age (time-bounded access)
|
|
343
|
+
if (this.config.maxAttestationAge > 0 && age > this.config.maxAttestationAge) {
|
|
344
|
+
errors.push(`Attestation too old (age: ${Math.floor(age)}s, max: ${this.config.maxAttestationAge}s)`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check schema allowlist
|
|
348
|
+
if (this.config.allowedSchemas.length > 0) {
|
|
349
|
+
if (!this.config.allowedSchemas.includes(attestation.schema)) {
|
|
350
|
+
errors.push(`Schema '${attestation.schema}' not in allowed list`)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check issuer allowlist
|
|
355
|
+
if (this.config.allowedIssuers.length > 0) {
|
|
356
|
+
if (!this.config.allowedIssuers.includes(attestation.issuer)) {
|
|
357
|
+
errors.push(`Issuer '${attestation.issuer}' not in allowed list`)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Run custom verification if provided
|
|
362
|
+
if (errors.length === 0 && this.config.customVerifier) {
|
|
363
|
+
try {
|
|
364
|
+
const customValid = await this.config.customVerifier(attestation)
|
|
365
|
+
if (!customValid) {
|
|
366
|
+
errors.push('Custom verification failed')
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
errors.push(`Custom verification error: ${e instanceof Error ? e.message : 'unknown'}`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
valid: errors.length === 0,
|
|
375
|
+
errors,
|
|
376
|
+
metadata: {
|
|
377
|
+
verificationMethod: this.config.verifyOnChain ? 'on-chain' : 'api',
|
|
378
|
+
schemaVersion: attestation.schema,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Revoke a previously derived viewing key
|
|
385
|
+
*
|
|
386
|
+
* @param attestation - The attestation whose key should be revoked
|
|
387
|
+
* @returns Whether revocation was successful
|
|
388
|
+
*/
|
|
389
|
+
revokeViewingKey(attestation: RangeSASAttestation): boolean {
|
|
390
|
+
const key = this.getCacheKey(attestation)
|
|
391
|
+
const deleted = this.derivedKeys.delete(key)
|
|
392
|
+
if (deleted) {
|
|
393
|
+
// Remove from LRU order
|
|
394
|
+
const index = this.cacheOrder.indexOf(key)
|
|
395
|
+
if (index !== -1) {
|
|
396
|
+
this.cacheOrder.splice(index, 1)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return deleted
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if a viewing key has been derived for an attestation
|
|
404
|
+
*
|
|
405
|
+
* @param attestation - The attestation to check
|
|
406
|
+
* @returns Whether a key exists
|
|
407
|
+
*/
|
|
408
|
+
hasViewingKey(attestation: RangeSASAttestation): boolean {
|
|
409
|
+
const key = this.getCacheKey(attestation)
|
|
410
|
+
return this.derivedKeys.has(key)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the current cache size
|
|
415
|
+
*
|
|
416
|
+
* @returns Number of cached viewing keys
|
|
417
|
+
*/
|
|
418
|
+
getCacheSize(): number {
|
|
419
|
+
return this.derivedKeys.size
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Clear all cached viewing keys
|
|
424
|
+
*/
|
|
425
|
+
clearCache(): void {
|
|
426
|
+
this.derivedKeys.clear()
|
|
427
|
+
this.cacheOrder.length = 0
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Private Methods ────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Add a key to cache with LRU eviction
|
|
434
|
+
*/
|
|
435
|
+
private cacheKey(key: string, viewingKey: ViewingKey): void {
|
|
436
|
+
// Evict oldest entries if cache is full
|
|
437
|
+
while (this.derivedKeys.size >= this.config.maxCacheSize && this.cacheOrder.length > 0) {
|
|
438
|
+
const oldest = this.cacheOrder.shift()
|
|
439
|
+
if (oldest) {
|
|
440
|
+
this.derivedKeys.delete(oldest)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.derivedKeys.set(key, viewingKey)
|
|
445
|
+
this.cacheOrder.push(key)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Update LRU order for a cache key (move to end)
|
|
450
|
+
*/
|
|
451
|
+
private updateCacheOrder(key: string): void {
|
|
452
|
+
const index = this.cacheOrder.indexOf(key)
|
|
453
|
+
if (index !== -1) {
|
|
454
|
+
this.cacheOrder.splice(index, 1)
|
|
455
|
+
this.cacheOrder.push(key)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Derive a viewing key from an attestation
|
|
461
|
+
*/
|
|
462
|
+
private deriveKeyFromAttestation(attestation: RangeSASAttestation): ViewingKey {
|
|
463
|
+
const masterKeyHex = this.config.masterViewingKey.key.startsWith('0x')
|
|
464
|
+
? this.config.masterViewingKey.key.slice(2)
|
|
465
|
+
: this.config.masterViewingKey.key
|
|
466
|
+
const masterKeyBytes = hexToBytes(masterKeyHex)
|
|
467
|
+
|
|
468
|
+
// Create derivation data from attestation
|
|
469
|
+
// Include signature to cryptographically bind keys to attestation
|
|
470
|
+
// This prevents forgery attacks where attacker uses same uid/subject
|
|
471
|
+
const derivationData = utf8ToBytes(
|
|
472
|
+
`SIP-RANGE-SAS:${attestation.uid}:${attestation.subject}:${attestation.schema}:${attestation.signature}`
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
// HMAC-SHA512 derivation
|
|
476
|
+
const derived = hmac(sha512, masterKeyBytes, derivationData)
|
|
477
|
+
const keyBytes = derived.slice(0, 32)
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const key = `0x${bytesToHex(keyBytes)}` as HexString
|
|
481
|
+
const hashBytes = sha256(keyBytes)
|
|
482
|
+
const hash = `0x${bytesToHex(hashBytes)}` as Hash
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
key,
|
|
486
|
+
path: `${this.config.masterViewingKey.path}/sas/${attestation.uid.slice(0, 8)}`,
|
|
487
|
+
hash,
|
|
488
|
+
}
|
|
489
|
+
} finally {
|
|
490
|
+
secureWipe(masterKeyBytes)
|
|
491
|
+
secureWipe(derived)
|
|
492
|
+
secureWipe(keyBytes)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get cache key for an attestation
|
|
498
|
+
*
|
|
499
|
+
* Includes schema and issuer to prevent cache poisoning attacks where
|
|
500
|
+
* an attacker could evict legitimate cache entries with same uid:subject.
|
|
501
|
+
*/
|
|
502
|
+
private getCacheKey(attestation: RangeSASAttestation): string {
|
|
503
|
+
return `${attestation.uid}:${attestation.subject}:${attestation.schema}:${attestation.issuer}`
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ─── Utility Functions ────────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create a mock attestation for testing
|
|
511
|
+
*
|
|
512
|
+
* @param overrides - Fields to override
|
|
513
|
+
* @returns Mock attestation
|
|
514
|
+
*/
|
|
515
|
+
export function createMockAttestation(
|
|
516
|
+
overrides: Partial<RangeSASAttestation> = {}
|
|
517
|
+
): RangeSASAttestation {
|
|
518
|
+
const now = Math.floor(Date.now() / 1000)
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
uid: `sas_${Math.random().toString(36).slice(2, 10)}`,
|
|
522
|
+
schema: AttestationSchema.RANGE_KYC_V1,
|
|
523
|
+
issuer: 'range-protocol',
|
|
524
|
+
subject: '11111111111111111111111111111112', // System program (placeholder)
|
|
525
|
+
data: {
|
|
526
|
+
level: 'institutional',
|
|
527
|
+
jurisdiction: 'US',
|
|
528
|
+
verifiedAt: now,
|
|
529
|
+
},
|
|
530
|
+
timestamp: now,
|
|
531
|
+
expiresAt: now + 365 * 24 * 60 * 60, // 1 year
|
|
532
|
+
signature: '0x' + '00'.repeat(64),
|
|
533
|
+
revoked: false,
|
|
534
|
+
...overrides,
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Verify attestation signature (placeholder for real implementation)
|
|
540
|
+
*
|
|
541
|
+
* ⚠️ WARNING: This is a stub that always returns true!
|
|
542
|
+
* Do NOT use in production without implementing real verification.
|
|
543
|
+
*
|
|
544
|
+
* In production, this would:
|
|
545
|
+
* 1. Fetch the issuer's public key from Range SAS registry
|
|
546
|
+
* 2. Verify the signature against the attestation data
|
|
547
|
+
* 3. Check on-chain state if verifyOnChain is enabled
|
|
548
|
+
*
|
|
549
|
+
* @param attestation - The attestation to verify
|
|
550
|
+
* @returns Whether the signature is valid (currently always true - STUB)
|
|
551
|
+
*
|
|
552
|
+
* @see https://github.com/sip-protocol/sip-protocol/issues/448 for implementation tracking
|
|
553
|
+
*/
|
|
554
|
+
export async function verifyAttestationSignature(
|
|
555
|
+
_attestation: RangeSASAttestation
|
|
556
|
+
): Promise<boolean> {
|
|
557
|
+
// TODO: Implement real signature verification with Range SAS
|
|
558
|
+
// This would involve:
|
|
559
|
+
// 1. Fetching issuer public key from Range registry
|
|
560
|
+
// 2. Reconstructing the signed message
|
|
561
|
+
// 3. Verifying Ed25519 signature
|
|
562
|
+
console.warn(
|
|
563
|
+
'[Range SAS] verifyAttestationSignature is a STUB - always returns true. ' +
|
|
564
|
+
'Implement real Ed25519 signature verification before production use.'
|
|
565
|
+
)
|
|
566
|
+
return true
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Fetch attestation from Range API
|
|
571
|
+
*
|
|
572
|
+
* ⚠️ WARNING: This is a stub that always returns null!
|
|
573
|
+
* Do NOT rely on this in production without implementing real API calls.
|
|
574
|
+
*
|
|
575
|
+
* @param uid - Attestation UID
|
|
576
|
+
* @param apiEndpoint - Range API endpoint
|
|
577
|
+
* @returns The attestation if found (currently always null - STUB)
|
|
578
|
+
*
|
|
579
|
+
* @see https://github.com/sip-protocol/sip-protocol/issues/448 for implementation tracking
|
|
580
|
+
*/
|
|
581
|
+
export async function fetchAttestation(
|
|
582
|
+
uid: string,
|
|
583
|
+
apiEndpoint: string = 'https://api.range.org/v1'
|
|
584
|
+
): Promise<RangeSASAttestation | null> {
|
|
585
|
+
// TODO: Implement real API call to Range
|
|
586
|
+
console.warn(
|
|
587
|
+
`[Range SAS] fetchAttestation is a STUB - returning null for ${uid}. ` +
|
|
588
|
+
`Would fetch from ${apiEndpoint}. Implement Range API integration before production use.`
|
|
589
|
+
)
|
|
590
|
+
return null
|
|
591
|
+
}
|