Skip to content

Commit

Permalink
feat(ssi-types): sd-jwt support
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Dec 5, 2023
1 parent 9e3fc6a commit b9154a0
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 29 deletions.
11 changes: 9 additions & 2 deletions packages/ssi-types/__tests__/uniform-claims.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as fs from 'fs'
import { CredentialMapper, ICredential, IVerifiableCredential, ICredentialSubject, W3CVerifiableCredential } from '../src'
import {
CredentialMapper,
ICredential,
IVerifiableCredential,
ICredentialSubject,
W3CVerifiableCredential,
JwtDecodedVerifiablePresentation,
} from '../src'

function getFile(path: string) {
return fs.readFileSync(path, 'utf-8')
Expand Down Expand Up @@ -138,7 +145,7 @@ describe('Uniform VP claims', () => {
it('JWT Decoded VP should populate response', () => {
const jwtEncodedVp = getFile('./packages/ssi-types/__tests__/vc_vp_examples/vp/vp_universityDegree.jwt')
const jwtDecodedVp = CredentialMapper.toWrappedVerifiablePresentation(jwtEncodedVp).decoded
const vp = CredentialMapper.toUniformPresentation(jwtDecodedVp)
const vp = CredentialMapper.toUniformPresentation(jwtDecodedVp as JwtDecodedVerifiablePresentation)
// vp should be decoded
expect(vp.holder).toEqual('did:example:ebfeb1f712ebc6f1c276e12ec21')
// vc should be decoded for a uniform vp
Expand Down
114 changes: 101 additions & 13 deletions packages/ssi-types/src/mapper/credential-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
W3CVerifiablePresentation,
WrappedVerifiableCredential,
WrappedVerifiablePresentation,
SdJwtDecodedVerifiableCredential,
SdJwtDecodedVerifiableCredentialPayload,
ICredential,
} from '../types'
import { ObjectUtils } from '../utils'

Expand All @@ -44,7 +47,9 @@ export class CredentialMapper {
}
}

static decodeVerifiableCredential(credential: OriginalVerifiableCredential): JwtDecodedVerifiableCredential | IVerifiableCredential {
static decodeVerifiableCredential(
credential: OriginalVerifiableCredential
): JwtDecodedVerifiableCredential | IVerifiableCredential | SdJwtDecodedVerifiableCredentialPayload {
if (CredentialMapper.isJwtEncoded(credential)) {
const payload = jwt_decode(credential as string) as JwtDecodedVerifiableCredential
const header = jwt_decode(credential as string, { header: true }) as Record<string, any>
Expand All @@ -57,9 +62,15 @@ export class CredentialMapper {
}
return payload
} else if (CredentialMapper.isJwtDecodedCredential(credential)) {
return credential as JwtDecodedVerifiableCredential
return credential
} else if (CredentialMapper.isJsonLdAsString(credential)) {
return JSON.parse(credential as string) as IVerifiableCredential
} else if (CredentialMapper.isSdJwtEncoded(credential)) {
throw new Error(
'Decoding SD-JWT VC is not supported at the moment. You must provide the decoded SD-JWT according to the SdJwtDecodedVerifiableCredential interface'
)
} else if (CredentialMapper.isSdJwtDecodedCredential(credential)) {
return credential.decodedPayload
} else {
return credential as IVerifiableCredential
}
Expand All @@ -69,6 +80,18 @@ export class CredentialMapper {
originalPresentation: OriginalVerifiablePresentation,
opts?: { maxTimeSkewInMS?: number }
): WrappedVerifiablePresentation {
if (CredentialMapper.isSdJwtDecodedCredential(originalPresentation)) {
return {
type: OriginalType.SD_JWT_VC_DECODED,
format: 'vc+sd-jwt',
original: originalPresentation,
presentation: originalPresentation.decodedPayload,
decoded: originalPresentation.decodedPayload,
// NOTE: the SD-JWT IS the credential as well as the presentation, but maybe the SD-JWT payload should be the credential
// while the KB-JWT is the presentation?
vcs: [],
}
}
const proof = CredentialMapper.getFirstProof(originalPresentation)
const original =
typeof originalPresentation !== 'string' && CredentialMapper.hasJWTProofType(originalPresentation) ? proof?.jwt : originalPresentation
Expand Down Expand Up @@ -123,6 +146,16 @@ export class CredentialMapper {
verifiableCredential: OriginalVerifiableCredential,
opts?: { maxTimeSkewInMS?: number }
): WrappedVerifiableCredential {
if (CredentialMapper.isSdJwtDecodedCredential(verifiableCredential)) {
return {
type: OriginalType.SD_JWT_VC_DECODED,
format: 'vc+sd-jwt',
original: verifiableCredential,
credential: verifiableCredential.decodedPayload,
decoded: verifiableCredential.decodedPayload,
}
}

const proof = CredentialMapper.getFirstProof(verifiableCredential)
const original = CredentialMapper.hasJWTProofType(verifiableCredential) && proof ? proof.jwt ?? verifiableCredential : verifiableCredential
if (!original) {
Expand Down Expand Up @@ -151,20 +184,58 @@ export class CredentialMapper {
}
}

public static isJwtEncoded(original: OriginalVerifiableCredential | OriginalVerifiablePresentation) {
return ObjectUtils.isString(original) && (original as string).startsWith('ey')
public static isJwtEncoded(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is string {
return ObjectUtils.isString(original) && original.startsWith('ey') && !original.includes('~')
}

public static isSdJwtEncoded(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is string {
return ObjectUtils.isString(original) && original.startsWith('ey') && original.includes('~')
}

public static isW3cCredential(credential: ICredential | SdJwtDecodedVerifiableCredentialPayload): credential is ICredential {
return '@context' in credential && ((credential as ICredential).type?.includes('VerifiableCredential') || false)
}

private static isJsonLdAsString(original: OriginalVerifiableCredential | OriginalVerifiablePresentation) {
return ObjectUtils.isString(original) && (original as string).includes('@context')
public static isW3cPresentation(
presentation: UniformVerifiablePresentation | IPresentation | SdJwtDecodedVerifiableCredentialPayload
): presentation is IPresentation {
return '@context' in presentation && ((presentation as IPresentation).type?.includes('VerifiablePresentation') || false)
}

public static isJwtDecodedCredential(original: OriginalVerifiableCredential): boolean {
return (<JwtDecodedVerifiableCredential>original)['vc'] !== undefined && (<JwtDecodedVerifiableCredential>original)['iss'] !== undefined
public static isSdJwtDecodedCredentialPayload(
credential: ICredential | SdJwtDecodedVerifiableCredentialPayload
): credential is SdJwtDecodedVerifiableCredentialPayload {
return 'vct' in credential
}

public static areOriginalVerifiableCredentialsEqual(firstOriginal: OriginalVerifiableCredential, secondOriginal: OriginalVerifiableCredential) {
// String (e.g. encoded jwt)
if (typeof firstOriginal === 'string' || typeof secondOriginal === 'string') {
return firstOriginal === secondOriginal
} else if (CredentialMapper.isSdJwtDecodedCredential(firstOriginal) || CredentialMapper.isSdJwtDecodedCredential(secondOriginal)) {
return firstOriginal.compactSdJwtVc === secondOriginal.compactSdJwtVc
} else {
// JSON-LD or decoded JWT. (should we compare the signatures instead?)
return JSON.stringify(secondOriginal.proof) === JSON.stringify(firstOriginal.proof)
}
}

public static isJwtDecodedPresentation(original: OriginalVerifiablePresentation): boolean {
return (<JwtDecodedVerifiablePresentation>original)['vp'] !== undefined && (<JwtDecodedVerifiablePresentation>original)['iss'] !== undefined
private static isJsonLdAsString(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is string {
return ObjectUtils.isString(original) && original.includes('@context')
}

public static isSdJwtDecodedCredential(
original: OriginalVerifiableCredential | OriginalVerifiablePresentation
): original is SdJwtDecodedVerifiableCredential {
return (<SdJwtDecodedVerifiableCredential>original).compactSdJwtVc !== undefined
}

public static isJwtDecodedCredential(original: OriginalVerifiableCredential): original is JwtDecodedVerifiableCredential {
return (<JwtDecodedVerifiableCredential>original).vc !== undefined && (<JwtDecodedVerifiableCredential>original).iss !== undefined
}

public static isJwtDecodedPresentation(original: OriginalVerifiablePresentation): original is JwtDecodedVerifiablePresentation {
return (<JwtDecodedVerifiablePresentation>original).vp !== undefined && (<JwtDecodedVerifiablePresentation>original).iss !== undefined
}

static jwtEncodedPresentationToUniformPresentation(
Expand Down Expand Up @@ -226,6 +297,9 @@ export class CredentialMapper {
maxTimeSkewInMS?: number
}
): IVerifiableCredential {
if (CredentialMapper.isSdJwtDecodedCredential(verifiableCredential)) {
throw new Error('Converting SD-JWT VC to uniform VC is not supported.')
}
const original =
typeof verifiableCredential !== 'string' && CredentialMapper.hasJWTProofType(verifiableCredential)
? CredentialMapper.getFirstProof(verifiableCredential)?.jwt
Expand All @@ -251,6 +325,10 @@ export class CredentialMapper {
presentation: OriginalVerifiablePresentation,
opts?: { maxTimeSkewInMS?: number; addContextIfMissing?: boolean }
): IVerifiablePresentation {
if (CredentialMapper.isSdJwtDecodedCredential(presentation)) {
throw new Error('Converting SD-JWT VC to uniform VP is not supported.')
}

const proof = CredentialMapper.getFirstProof(presentation)
const original = typeof presentation !== 'string' && CredentialMapper.hasJWTProofType(presentation) ? proof?.jwt : presentation
if (!original) {
Expand Down Expand Up @@ -459,11 +537,21 @@ export class CredentialMapper {
}

static detectDocumentType(
document: W3CVerifiableCredential | W3CVerifiablePresentation | JwtDecodedVerifiableCredential | JwtDecodedVerifiablePresentation
document:
| W3CVerifiableCredential
| W3CVerifiablePresentation
| JwtDecodedVerifiableCredential
| JwtDecodedVerifiablePresentation
| SdJwtDecodedVerifiableCredential
): DocumentFormat {
if (typeof document === 'string') {
return this.isJsonLdAsString(document) ? DocumentFormat.JSONLD : DocumentFormat.JWT
if (this.isJsonLdAsString(document)) {
return DocumentFormat.JSONLD
} else if (this.isJwtEncoded(document)) {
return DocumentFormat.JWT
} else if (this.isSdJwtEncoded(document) || this.isSdJwtDecodedCredential(document as any)) {
return DocumentFormat.SD_JWT_VC
}

const proofs = 'vc' in document ? document.vc.proof : 'vp' in document ? document.vp.proof : (<IVerifiableCredential>document).proof
const proof: IProof = Array.isArray(proofs) ? proofs[0] : proofs

Expand Down
1 change: 1 addition & 0 deletions packages/ssi-types/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './did'
export * from './pex'
export * from './vc'
export * from './generic'
export * from './sd-jwt-vc'
120 changes: 120 additions & 0 deletions packages/ssi-types/src/types/sd-jwt-vc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
type JsonValue = string | number | boolean | { [x: string]: JsonValue | undefined } | Array<JsonValue>

type SdJwtJsonValue =
| string
| number
| boolean
| {
[x: string]: SdJwtJsonValue | undefined
_sd?: string[]
}
| Array<SdJwtJsonValue | { '...': string }>

/**
* Decoded 'pretty' SD JWT Verifiable Credential. This representation has all the `_sd` properties
* removed, and includes the disclosures directly within the payload.
*/
export interface SdJwtDecodedVerifiableCredentialPayload {
vct: string
iss: string
iat: number
nbf?: number
exp?: number
cnf?: {
jwk?: any
// TODO: add other cnf properties
}
status?: {
idx: number
uri: string
}
sub?: string

[key: string]: JsonValue | undefined
}

/**
* The signed payload of an SD-JWT. Includes fields such as `_sd`, `...` and `_sd_alg`
*/
interface SdJwtSignedVerifiableCredentialPayload extends SdJwtDecodedVerifiableCredentialPayload {
// Only present if there are any selectively discloseable claims
_sd?: string[]
_sd_alg?: string

[x: string]: SdJwtJsonValue | undefined
}

type SdJwtFrameValue = boolean | Array<SdJwtFrameValue> | { [x: string]: SdJwtFrameValue }
export type SdJwtDisclosureFrame = Record<string, SdJwtFrameValue>
export type SdJwtPresentationFrame = Record<string, SdJwtFrameValue>

/**
* Input for creating a SD JWT Verifiable Credential. This representation optionally includes the disclosure frame,
* (as `__disclosureFrame`) to indicate which fields in the signed SD-JWT should be selectively discloseable
*/
export interface SdJwtCredentialInput extends SdJwtDecodedVerifiableCredentialPayload {
/**
* Disclosure frame, indicating which fields in the signed SD-JWT should be selectively discloseable
* Will be removed from the actual SD-JWT payload before signing
*/
__disclosureFrame?: SdJwtDisclosureFrame
}

/**
* The presentation of an SD-JWT. It is the same as an SD-JWT credential, with the addition
* of an optional key binding JWT that can be included.
*/
export interface SdJwtDecodedVerifiablePresentation extends SdJwtDecodedVerifiableCredential {
/**
* Compact JWT encoding of the key binding (kb) JSON Web Token. This property will be included
* when the SD-JWT presentation includes a key binding JWT.
*/
compactKbJwt?: string
}

/**
* The decoded SD JWT Verifiable Credential. This representation includes multiple representations of the
* same SD-JWT, and allows to fully process an SD-JWT, as well as create a presentation SD-JWT (minus the KB-JWT) by removing
* certain disclosures from the compact SD-JWT.
*
* This representation is useful as it doesn't require a hasher implementation to match the different digests in the signed SD-JWT
* payload, with the different disclosures.
*/
export interface SdJwtDecodedVerifiableCredential {
/**
* The compact sd jwt is the sd-jwt encoded as string. It is a normal JWT,
* with the disclosures and kb-jwt appended separated by ~ */
compactSdJwtVc: string

/**
* The disclosures included within the SD-JWT in both encoded and decoded format.
* The digests are also included, and allows the disclosures to be linked against
* the digests in the signed payload.
*/
disclosures: Array<{
// The encoded disclosure
encoded: string

// The decoded disclosure, in format [salt, claim, value] or in case of array entry [salt, value]
decoded: [string, string, JsonValue] | [string, JsonValue]

// Digest over disclosure, can be used to match against a value within the SD JWT payload
digest: string
}>

/**
* The signed payload is the payload of the sd-jwt that is actually signed, and that includes
* the `_sd` and `...` digests.
*/
signedPayload: SdJwtSignedVerifiableCredentialPayload

/**
* The decoded payload is the payload when all `_sd` and `...` digests have been replaced
* by the actual values from the disclosures. This format could also be seen as the 'pretty`
* version of the SD JWT payload.
*
* This is useful for displaying the contents of the SD JWT VC to the user, or for example
* for querying the contents of the SD JWT VC using a PEX presentation definition path.
*/
decodedPayload: SdJwtDecodedVerifiableCredentialPayload
}
Loading

0 comments on commit b9154a0

Please sign in to comment.