@sphereon/ssi-types
Version:
SSI Common Types
373 lines (334 loc) • 13.2 kB
text/typescript
import { OriginalType, WrappedVerifiableCredential, WrappedVerifiablePresentation } from './vc'
import { decodeSdJwt, decodeSdJwtSync, getClaims, getClaimsSync } from '@sd-jwt/decode'
import { CompactJWT, IVerifiableCredential } from './w3c-vc'
import { IProofPurpose, IProofType } from './did'
import { OrPromise } from './generic'
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
kid?: string
}
status?: {
idx: number
uri: string
}
sub?: string
[key: string]: JsonValue | undefined
}
/**
* Represents a selective disclosure JWT vc in compact form.
*/
export type CompactSdJwtVc = string
/**
* 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
}
export type SdJwtDecodedDisclosure = [string, string, JsonValue] | [string, JsonValue]
export interface SdJwtDisclosure {
// The encoded disclosure
encoded: string
// The decoded disclosure, in format [salt, claim, value] or in case of array entry [salt, value]
decoded: SdJwtDecodedDisclosure
// Digest over disclosure, can be used to match against a value within the SD JWT payload
digest: 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<SdJwtDisclosure>
/**
* 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
/**
* Key binding JWT
*/
kbJwt?: {
header: SdJwtVcKbJwtHeader
payload: SdJwtVcKbJwtPayload
compact?: CompactJWT
}
}
export interface SdJwtVcKbJwtHeader {
typ: 'kb+jwt'
alg: string
[x: string]: any
}
export interface SdJwtVcKbJwtPayload {
iat: number
aud: string
nonce: string
sd_hash: string
[key: string]: unknown
}
export interface WrappedSdJwtVerifiableCredential {
/**
* Original VC that we've received. Can be either the encoded or decoded variant.
*/
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
/**
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT as the `decoded` property
* is used in e.g. PEX to check for path filters from fields. The full decoded credential can be found in the `credential` field.
*/
decoded: SdJwtDecodedVerifiableCredentialPayload
/**
* Type of this credential.
*/
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
/**
* The claim format, typically used during exchange transport protocols
*/
format: 'vc+sd-jwt'
/**
* Internal stable representation of a Credential
*/
credential: SdJwtDecodedVerifiableCredential
}
export type HasherSync = (data: string | ArrayBuffer, alg: string) => Uint8Array
export type Hasher = (data: string | ArrayBuffer, alg: string) => OrPromise<Uint8Array>
export interface WrappedSdJwtVerifiablePresentation {
/**
* Original VP that we've received. Can be either the encoded or decoded variant.
*/
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
/**
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT.
*/
decoded: SdJwtDecodedVerifiableCredentialPayload
/**
* Type of this Presentation.
*/
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
/**
* The claim format, typically used during exchange transport protocols
*/
format: 'vc+sd-jwt'
/**
* Internal stable representation of a Presentation
*/
presentation: SdJwtDecodedVerifiableCredential
/**
* Wrapped Verifiable Credentials belonging to the Presentation. Will always be an array
* with a single SdJwtVerifiableCredential entry.
*/
vcs: [WrappedSdJwtVerifiableCredential]
}
export function isWrappedSdJwtVerifiableCredential(vc: WrappedVerifiableCredential): vc is WrappedSdJwtVerifiableCredential {
return vc.format === 'vc+sd-jwt'
}
export function isWrappedSdJwtVerifiablePresentation(vp: WrappedVerifiablePresentation): vp is WrappedSdJwtVerifiablePresentation {
return vp.format === 'vc+sd-jwt'
}
/**
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
* signed payload, decoded payload and the compact SD-JWT vc.
*
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
*/
export function decodeSdJwtVc(compactSdJwtVc: CompactSdJwtVc, hasher: HasherSync): SdJwtDecodedVerifiableCredential {
const { jwt, disclosures, kbJwt } = decodeSdJwtSync(compactSdJwtVc, hasher)
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
const decodedPayload = getClaimsSync(signedPayload, disclosures, hasher)
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
return {
compactSdJwtVc,
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
disclosures: disclosures.map((d) => {
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
return {
decoded: decoded as SdJwtDecodedDisclosure,
digest: d._digest,
encoded: d.encode(),
} satisfies SdJwtDisclosure
}),
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
...(compactKeyBindingJwt &&
kbJwt && {
kbJwt: {
header: kbJwt.header as SdJwtVcKbJwtHeader,
compact: compactKeyBindingJwt,
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
},
}),
}
}
/**
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
* signed payload, decoded payload and the compact SD-JWT vc.
*
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
*/
export async function decodeSdJwtVcAsync(compactSdJwtVc: CompactSdJwtVc, hasher: Hasher): Promise<SdJwtDecodedVerifiableCredential> {
const { jwt, disclosures, kbJwt } = await decodeSdJwt(compactSdJwtVc, hasher)
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
const decodedPayload = await getClaims(signedPayload, disclosures, hasher)
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
return {
compactSdJwtVc,
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
disclosures: disclosures.map((d) => {
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
return {
decoded: decoded as SdJwtDecodedDisclosure,
digest: d._digest,
encoded: d.encode(),
} satisfies SdJwtDisclosure
}),
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
...(compactKeyBindingJwt &&
kbJwt && {
kbJwt: {
header: kbJwt.header as SdJwtVcKbJwtHeader,
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
compact: compactKeyBindingJwt,
},
}),
}
}
export const sdJwtDecodedCredentialToUniformCredential = (
decoded: SdJwtDecodedVerifiableCredential,
opts?: { maxTimeSkewInMS?: number },
): IVerifiableCredential => {
const { decodedPayload } = decoded
const { exp, nbf, iss, iat, vct, cnf, status, sub, jti } = decodedPayload
const maxSkewInMS = opts?.maxTimeSkewInMS ?? 1500
const expirationDate = jwtDateToISOString({ jwtClaim: exp, claimName: 'exp' })
let issuanceDateStr = jwtDateToISOString({ jwtClaim: iat, claimName: 'iat' })
let nbfDateAsStr: string | undefined
if (nbf) {
nbfDateAsStr = jwtDateToISOString({ jwtClaim: nbf, claimName: 'nbf' })
if (issuanceDateStr && nbfDateAsStr && issuanceDateStr !== nbfDateAsStr) {
const diff = Math.abs(new Date(nbfDateAsStr).getTime() - new Date(iss).getTime())
if (!maxSkewInMS || diff > maxSkewInMS) {
throw Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${iss})`)
}
}
issuanceDateStr = nbfDateAsStr
}
const issuanceDate = issuanceDateStr
if (!issuanceDate) {
throw Error(`JWT issuance date is required but was not present`)
}
// Filter out the fields we don't want in credentialSubject
const excludedFields = new Set(['vct', 'cnf', 'iss', 'iat', 'exp', 'nbf', 'jti', 'sub'])
const credentialSubject = Object.entries(decodedPayload).reduce(
(acc, [key, value]) => {
if (
!excludedFields.has(key) &&
value !== undefined &&
value !== '' &&
!(typeof value === 'object' && value !== null && Object.keys(value).length === 0)
) {
acc[key] = value
}
return acc
},
{} as Record<string, any>,
)
const credential: Omit<IVerifiableCredential, 'issuer' | 'issuanceDate'> = {
type: [vct], // SDJwt is not a W3C VC, so no VerifiableCredential
'@context': [], // SDJwt has no JSON-LD by default. Certainly not the VC DM1 default context for JSON-LD
credentialSubject: {
...credentialSubject,
id: credentialSubject.id ?? sub ?? jti,
},
issuanceDate,
expirationDate,
issuer: iss,
...(cnf && { cnf }),
...(status && { status }),
proof: {
type: IProofType.SdJwtProof2024,
created: nbfDateAsStr ?? issuanceDate,
proofPurpose: IProofPurpose.authentication,
verificationMethod: iss,
jwt: decoded.compactSdJwtVc,
},
}
return credential as IVerifiableCredential
}
const jwtDateToISOString = ({
jwtClaim,
claimName,
isRequired = false,
}: {
jwtClaim?: number
claimName: string
isRequired?: boolean
}): string | undefined => {
if (jwtClaim) {
const claim = parseInt(jwtClaim.toString())
// change JWT seconds to millisecond for the date
return new Date(claim * (claim < 9999999999 ? 1000 : 1)).toISOString().replace(/\.000Z/, 'Z')
} else if (isRequired) {
throw Error(`JWT claim ${claimName} is required but was not present`)
}
return undefined
}