-
Notifications
You must be signed in to change notification settings - Fork 24
/
Claim.ts
299 lines (285 loc) · 11.8 KB
/
Claim.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
/**
* Copyright (c) 2018-2023, BOTLabs GmbH.
*
* This source code is licensed under the BSD 4-Clause "Original" license
* found in the LICENSE file in the root directory of this source tree.
*/
/**
* Claims are a core building block of the KILT SDK. A claim represents **something an entity claims about itself**. Once created, a claim can be used to create a [[Credential]].
*
* A claim object has:
* * contents - among others, the pure content of a claim, for example `"isOver18": true`;
* * a [[CType]] that represents its data structure.
*
* A claim object's owner is (should be) the same entity as the claimer.
*
* @packageDocumentation
*/
import { hexToBn } from '@polkadot/util'
import type {
DidUri,
IClaim,
ICType,
PartialClaim,
HexString,
} from '@kiltprotocol/types'
import { Crypto, DataUtils, SDKErrors } from '@kiltprotocol/utils'
import * as Did from '@kiltprotocol/did'
import * as CType from '../ctype/index.js'
const VC_VOCAB = 'https://www.w3.org/2018/credentials#'
/**
* Produces JSON-LD readable representations of [[IClaim]]['contents']. This is done by implicitly or explicitly transforming property keys to globally unique predicates.
* Where possible these predicates are taken directly from the Verifiable Credentials vocabulary. Properties that are unique to a [[CType]] are transformed into predicates by prepending the [[CType]][schema][$id].
*
* @param claim A (partial) [[IClaim]] from to build a JSON-LD representation from. The `cTypeHash` property is required.
* @param expanded Return an expanded instead of a compacted representation. While property transformation is done explicitly in the expanded format, it is otherwise done implicitly via adding JSON-LD's reserved `@context` properties while leaving [[IClaim]][contents] property keys untouched.
* @returns An object which can be serialized into valid JSON-LD representing an [[IClaim]]'s ['contents'].
*/
function jsonLDcontents(
claim: PartialClaim,
expanded = true
): Record<string, unknown> {
const { cTypeHash, contents, owner } = claim
if (!cTypeHash) throw new SDKErrors.CTypeHashMissingError()
const vocabulary = `${CType.hashToId(cTypeHash)}#`
const result: Record<string, unknown> = {}
if (owner) result['@id'] = owner
if (!expanded) {
return {
...result,
'@context': { '@vocab': vocabulary },
...contents,
}
}
Object.entries(contents || {}).forEach(([key, value]) => {
result[vocabulary + key] = value
})
return result
}
/**
* Produces JSON-LD readable representations of KILT claims. This is done by implicitly or explicitly transforming property keys to globally unique predicates.
* Where possible these predicates are taken directly from the Verifiable Credentials vocabulary. Properties that are unique to a [[CType]] are transformed into predicates by prepending the [[CType]][schema][$id].
*
* @param claim A (partial) [[IClaim]] from to build a JSON-LD representation from. The `cTypeHash` property is required.
* @param expanded Return an expanded instead of a compacted representation. While property transformation is done explicitly in the expanded format, it is otherwise done implicitly via adding JSON-LD's reserved `@context` properties while leaving [[IClaim]][contents] property keys untouched.
* @returns An object which can be serialized into valid JSON-LD representing an [[IClaim]].
*/
export function toJsonLD(
claim: PartialClaim,
expanded = true
): Record<string, unknown> {
const credentialSubject = jsonLDcontents(claim, expanded)
const prefix = expanded ? VC_VOCAB : ''
const result = {
[`${prefix}credentialSubject`]: credentialSubject,
}
result[`${prefix}credentialSchema`] = {
'@id': CType.hashToId(claim.cTypeHash),
}
if (!expanded) result['@context'] = { '@vocab': VC_VOCAB }
return result
}
function makeStatementsJsonLD(claim: PartialClaim): string[] {
const normalized = jsonLDcontents(claim, true)
return Object.entries(normalized).map(([key, value]) =>
JSON.stringify({ [key]: value })
)
}
/**
* Produces salted hashes of individual statements comprising a (partial) [[IClaim]] to enable selective disclosure of contents. Can also be used to reproduce hashes for the purpose of validation.
*
* @param claim Full or partial [[IClaim]] to produce statement hashes from.
* @param options Object containing optional parameters.
* @param options.canonicalisation Canonicalisation routine that produces an array of statement strings from the [IClaim]. Default produces individual `{"key":"value"}` JSON representations where keys are transformed to expanded JSON-LD.
* @param options.nonces Optional map of nonces as produced by this function.
* @param options.nonceGenerator Nonce generator as defined by [[hashStatements]] to be used if no `nonces` are given. Default produces random UUIDs (v4).
* @param options.hasher The hasher to be used. Required but defaults to 256 bit blake2 over `${nonce}${statement}`.
* @returns An array of salted `hashes` and a `nonceMap` where keys correspond to unsalted statement hashes.
*/
export function hashClaimContents(
claim: PartialClaim,
options: Crypto.HashingOptions & {
canonicalisation?: (claim: PartialClaim) => string[]
} = {}
): {
hashes: HexString[]
nonceMap: Record<string, string>
} {
// apply defaults
const defaults = { canonicalisation: makeStatementsJsonLD }
const canonicalisation = options.canonicalisation || defaults.canonicalisation
// use canonicalisation algorithm to make hashable statement strings
const statements = canonicalisation(claim)
// iterate over statements to produce salted hashes
const processed = Crypto.hashStatements(statements, options)
// produce array of salted hashes to add to credential
const hashes = processed
.map(({ saltedHash }) => saltedHash)
.sort((a, b) => hexToBn(a).cmp(hexToBn(b)))
// produce nonce map, where each nonce is keyed with the unsalted hash
const nonceMap = {}
processed.forEach(({ digest, nonce, statement }) => {
// throw if we can't map a digest to a nonce - this should not happen if the nonce map is complete and the credential has not been tampered with
if (!nonce) throw new SDKErrors.ClaimNonceMapMalformedError(statement)
nonceMap[digest] = nonce
}, {})
return { hashes, nonceMap }
}
/**
* Used to verify the hash list based proof over the set of disclosed attributes in a [[Claim]].
*
* @param claim Full or partial [[IClaim]] to verify proof against.
* @param proof Proof consisting of a map that matches nonces to statement digests and the resulting hashes.
* @param proof.nonces A map where a statement digest as produces by options.hasher is mapped to a nonce.
* @param proof.hashes Array containing hashes which are signed into the credential. Should result from feeding statement digests and nonces in proof.nonce to options.hasher.
* @param options Object containing optional parameters.
* @param options.canonicalisation Canonicalisation routine that produces an array of statement strings from the [IClaim]. Default produces individual `{"key":"value"}` JSON representations where keys are transformed to expanded JSON-LD.
* @param options.hasher The hasher to be used. Required but defaults to 256 bit blake2 over `${nonce}${statement}`.
*/
export function verifyDisclosedAttributes(
claim: PartialClaim,
proof: {
nonces: Record<string, string>
hashes: string[]
},
options: Pick<Crypto.HashingOptions, 'hasher'> & {
canonicalisation?: (claim: PartialClaim) => string[]
} = {}
): void {
// apply defaults
const defaults = { canonicalisation: makeStatementsJsonLD }
const canonicalisation = options.canonicalisation || defaults.canonicalisation
const { nonces } = proof
// use canonicalisation algorithm to make hashable statement strings
const statements = canonicalisation(claim)
// iterate over statements to produce salted hashes
const hashed = Crypto.hashStatements(statements, { ...options, nonces })
// check resulting hashes
const digestsInProof = Object.keys(nonces)
const { verified, errors } = hashed.reduce<{
verified: boolean
errors: Error[]
}>(
(status, { saltedHash, statement, digest, nonce }) => {
// check if the statement digest was contained in the proof and mapped it to a nonce
if (!digestsInProof.includes(digest) || !nonce) {
status.errors.push(new SDKErrors.NoProofForStatementError(statement))
return { ...status, verified: false }
}
// check if the hash is whitelisted in the proof
if (!proof.hashes.includes(saltedHash)) {
status.errors.push(
new SDKErrors.InvalidProofForStatementError(statement)
)
return { ...status, verified: false }
}
return status
},
{ verified: true, errors: [] }
)
if (verified !== true) {
throw new SDKErrors.ClaimUnverifiableError(
'One or more statements in the claim could not be verified',
{ cause: errors }
)
}
}
/**
* Checks whether the input meets all the required criteria of an [[IClaim]] object.
* Throws on invalid input.
*
* @param input The potentially only partial IClaim.
*/
export function verifyDataStructure(input: IClaim | PartialClaim): void {
if (!input.cTypeHash) {
throw new SDKErrors.CTypeHashMissingError()
}
if ('owner' in input) {
Did.validateUri(input.owner, 'Did')
}
if (input.contents !== undefined) {
Object.entries(input.contents).forEach(([key, value]) => {
if (
!key ||
typeof key !== 'string' ||
!['string', 'number', 'boolean', 'object'].includes(typeof value)
) {
throw new SDKErrors.ClaimContentsMalformedError()
}
})
}
DataUtils.verifyIsHex(input.cTypeHash, 256)
}
/**
* Verifies the data structure and schema of a Claim.
*
* @param claimInput IClaim to verify.
* @param cType ICType to verify claimInput's contents.
*/
export function verify(claimInput: IClaim, cType: ICType): void {
CType.verifyClaimAgainstSchema(claimInput.contents, cType)
verifyDataStructure(claimInput)
}
/**
* Builds a [[Claim]] from a [[CType]] which has nested [[CType]]s within the schema.
*
* @param cTypeInput A [[CType]] object that has nested [[CType]]s.
* @param nestedCType The array of [[CType]]s, which are used inside the main [[CType]].
* @param claimContents The data inside the [[Claim]].
* @param claimOwner The DID of the owner of the [[Claim]].
*
* @returns A [[Claim]] the owner can use.
*/
export function fromNestedCTypeClaim(
cTypeInput: ICType,
nestedCType: ICType[],
claimContents: IClaim['contents'],
claimOwner: DidUri
): IClaim {
CType.verifyClaimAgainstNestedSchemas(cTypeInput, nestedCType, claimContents)
const claim = {
cTypeHash: CType.idToHash(cTypeInput.$id),
contents: claimContents,
owner: claimOwner,
}
verifyDataStructure(claim)
return claim
}
/**
* Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[DidUri]].
*
* @param cType [[ICType]] for which the Claim will be built.
* @param claimContents IClaim['contents'] to be used as the pure contents of the instantiated Claim.
* @param claimOwner The DID to be used as the Claim owner.
* @returns A Claim object.
*/
export function fromCTypeAndClaimContents(
cType: ICType,
claimContents: IClaim['contents'],
claimOwner: DidUri
): IClaim {
CType.verifyDataStructure(cType)
CType.verifyClaimAgainstSchema(claimContents, cType)
const claim = {
cTypeHash: CType.idToHash(cType.$id),
contents: claimContents,
owner: claimOwner,
}
verifyDataStructure(claim)
return claim
}
/**
* Custom Type Guard to determine input being of type IClaim.
*
* @param input The potentially only partial IClaim.
*
* @returns Boolean whether input is of type IClaim.
*/
export function isIClaim(input: unknown): input is IClaim {
try {
verifyDataStructure(input as IClaim)
} catch (error) {
return false
}
return true
}