@sphereon/ssi-types
Version:
SSI Common Types
923 lines (852 loc) • 40.2 kB
text/typescript
import { IssuerType } from '@veramo/core'
import {
decodeMdocDeviceResponse,
decodeMdocIssuerSigned,
decodeSdJwtVc,
decodeSdJwtVcAsync,
DocumentFormat,
getMdocDecodedPayload,
Hasher,
HasherSync,
ICredential,
IPresentation,
IProof,
IProofPurpose,
IProofType,
isWrappedMdocCredential,
isWrappedMdocPresentation,
isWrappedSdJwtVerifiableCredential,
isWrappedSdJwtVerifiablePresentation,
isWrappedW3CVerifiableCredential,
isWrappedW3CVerifiablePresentation,
IVerifiableCredential,
IVerifiablePresentation,
JwtDecodedVerifiableCredential,
JwtDecodedVerifiablePresentation,
mdocDecodedCredentialToUniformCredential,
MdocDeviceResponse,
MdocDocument,
MdocOid4vpMdocVpToken,
OriginalType,
OriginalVerifiableCredential,
OriginalVerifiablePresentation,
sdJwtDecodedCredentialToUniformCredential,
SdJwtDecodedVerifiableCredential,
SdJwtDecodedVerifiableCredentialPayload,
UniformVerifiablePresentation,
W3CVerifiableCredential,
W3CVerifiablePresentation,
WrappedMdocCredential,
WrappedSdJwtVerifiableCredential,
WrappedVerifiableCredential,
WrappedVerifiablePresentation,
WrappedW3CVerifiableCredential,
} from '../types'
import { defaultHasher, ObjectUtils } from '../utils'
import { com } from '@sphereon/kmp-mdoc-core'
import { jwtDecode } from 'jwt-decode'
import DeviceResponseCbor = com.sphereon.mdoc.data.device.DeviceResponseCbor
export const sha256 = (data: string | ArrayBuffer): Uint8Array => {
return defaultHasher(data, 'sha256')
}
export class CredentialMapper {
/**
* Decodes a compact SD-JWT vc to it's decoded variant. This method can be used when the hasher implementation used is Async, and therefore not suitable for usage
* with the other decode methods.
*/
static decodeSdJwtVcAsync(compactSdJwtVc: string, hasher: Hasher) {
return decodeSdJwtVcAsync(compactSdJwtVc, hasher ?? sha256)
}
/**
* Decodes a Verifiable Presentation to a uniform format.
*
* When decoding SD-JWT credentials, a hasher implementation must be provided. The hasher implementation must be sync. When using
* an async hasher implementation, use the decodeSdJwtVcAsync method instead and you can provide the decoded payload to methods
* instead of the compact SD-JWT.
*
* @param presentation
* @param hasher Hasher implementation to use for SD-JWT decoding.
*/
static decodeVerifiablePresentation(
presentation: OriginalVerifiablePresentation,
hasher?: HasherSync,
): JwtDecodedVerifiablePresentation | IVerifiablePresentation | SdJwtDecodedVerifiableCredential | MdocOid4vpMdocVpToken | MdocDeviceResponse {
if (CredentialMapper.isJwtEncoded(presentation)) {
const payload = jwtDecode(presentation as string) as JwtDecodedVerifiablePresentation
const header = jwtDecode(presentation as string, { header: true }) as Record<string, any>
payload.vp.proof = {
type: IProofType.JwtProof2020,
created: payload.nbf,
proofPurpose: IProofPurpose.authentication,
verificationMethod: header['kid'] ?? payload.iss,
jwt: presentation as string,
}
return payload
} else if (CredentialMapper.isJwtDecodedPresentation(presentation)) {
return presentation as JwtDecodedVerifiablePresentation
} else if (CredentialMapper.isSdJwtEncoded(presentation)) {
return decodeSdJwtVc(presentation, hasher ?? sha256)
} else if (CredentialMapper.isSdJwtDecodedCredential(presentation)) {
return presentation as SdJwtDecodedVerifiableCredential
} else if (CredentialMapper.isMsoMdocOid4VPEncoded(presentation)) {
return presentation as MdocOid4vpMdocVpToken
} else if (CredentialMapper.isMsoMdocDecodedPresentation(presentation)) {
return presentation as MdocDeviceResponse
} else if (CredentialMapper.isJsonLdAsString(presentation)) {
return JSON.parse(presentation as string) as IVerifiablePresentation
} else {
return presentation as IVerifiablePresentation
}
}
/**
* Decodes a Verifiable Credential to a uniform format.
*
* When decoding SD-JWT credentials, a hasher implementation must be provided. The hasher implementation must be sync. When using
* an async hasher implementation, use the decodeSdJwtVcAsync method instead and you can provide the decoded payload to methods
* instead of the compact SD-JWT.
*
* @param hasher Hasher implementation to use for SD-JWT decoding
*/
static decodeVerifiableCredential(
credential: OriginalVerifiableCredential,
hasher?: HasherSync,
): JwtDecodedVerifiableCredential | IVerifiableCredential | SdJwtDecodedVerifiableCredential {
if (CredentialMapper.isJwtEncoded(credential)) {
const payload = jwtDecode(credential as string) as JwtDecodedVerifiableCredential
const header = jwtDecode(credential as string, { header: true }) as Record<string, any>
payload.vc.proof = {
type: IProofType.JwtProof2020,
created: payload.nbf,
proofPurpose: IProofPurpose.authentication,
verificationMethod: header['kid'] ?? payload.iss,
jwt: credential as string,
}
return payload
} else if (CredentialMapper.isJwtDecodedCredential(credential)) {
return credential
} else if (CredentialMapper.isJsonLdAsString(credential)) {
return JSON.parse(credential as string) as IVerifiableCredential
} else if (CredentialMapper.isSdJwtEncoded(credential)) {
return decodeSdJwtVc(credential, hasher ?? sha256)
} else if (CredentialMapper.isSdJwtDecodedCredential(credential)) {
return credential
} else {
return credential as IVerifiableCredential
}
}
/**
* Converts a presentation to a wrapped presentation.
*
* When decoding SD-JWT credentials, a hasher implementation must be provided. The hasher implementation must be sync. When using
* an async hasher implementation, use the decodeSdJwtVcAsync method instead and you can provide the decoded payload to methods
* instead of the compact SD-JWT.
*
* @param hasher Hasher implementation to use for SD-JWT decoding
*/
static toWrappedVerifiablePresentation(
originalPresentation: OriginalVerifiablePresentation,
opts?: { maxTimeSkewInMS?: number; hasher?: HasherSync },
): WrappedVerifiablePresentation {
// MSO_MDOC
if (CredentialMapper.isMsoMdocDecodedPresentation(originalPresentation) || CredentialMapper.isMsoMdocOid4VPEncoded(originalPresentation)) {
let deviceResponse: MdocDeviceResponse
let originalType: OriginalType
if (CredentialMapper.isMsoMdocOid4VPEncoded(originalPresentation)) {
deviceResponse = decodeMdocDeviceResponse(originalPresentation)
originalType = OriginalType.MSO_MDOC_ENCODED
} else {
deviceResponse = originalPresentation
originalType = OriginalType.MSO_MDOC_DECODED
}
const mdocCredentials = deviceResponse.documents?.map(
(doc) => CredentialMapper.toWrappedVerifiableCredential(doc, opts) as WrappedMdocCredential,
)
if (!mdocCredentials || mdocCredentials.length === 0) {
throw new Error('could not extract any mdoc credentials from mdoc device response')
}
return {
type: originalType,
format: 'mso_mdoc',
original: originalPresentation,
presentation: deviceResponse,
decoded: deviceResponse,
vcs: mdocCredentials,
}
}
// SD-JWT
if (CredentialMapper.isSdJwtDecodedCredential(originalPresentation) || CredentialMapper.isSdJwtEncoded(originalPresentation)) {
let decodedPresentation: SdJwtDecodedVerifiableCredential
if (CredentialMapper.isSdJwtEncoded(originalPresentation)) {
decodedPresentation = decodeSdJwtVc(originalPresentation, opts?.hasher ?? sha256)
} else {
decodedPresentation = originalPresentation
}
return {
type: CredentialMapper.isSdJwtDecodedCredential(originalPresentation) ? OriginalType.SD_JWT_VC_DECODED : OriginalType.SD_JWT_VC_ENCODED,
format: 'vc+sd-jwt',
original: originalPresentation,
presentation: decodedPresentation,
decoded: decodedPresentation.decodedPayload,
// NOTE: we also include the SD-JWT VC as the VC, as the SD-JWT acts as both the VC and the VP
vcs: [CredentialMapper.toWrappedVerifiableCredential(originalPresentation, opts) as WrappedSdJwtVerifiableCredential],
}
}
// If the VP is not an encoded/decoded SD-JWT, we assume it will be a W3C VC
const proof = CredentialMapper.getFirstProof(originalPresentation)
const original =
typeof originalPresentation !== 'string' && CredentialMapper.hasJWTProofType(originalPresentation) ? proof?.jwt : originalPresentation
if (!original) {
throw Error(
'Could not determine original presentation, probably it was a converted JWT presentation, that is now missing the JWT value in the proof',
)
}
const decoded = CredentialMapper.decodeVerifiablePresentation(original) as IVerifiablePresentation | JwtDecodedVerifiablePresentation
const isJwtEncoded: boolean = CredentialMapper.isJwtEncoded(original)
const isJwtDecoded: boolean = CredentialMapper.isJwtDecodedPresentation(original)
const type = isJwtEncoded ? OriginalType.JWT_ENCODED : isJwtDecoded ? OriginalType.JWT_DECODED : OriginalType.JSONLD
const format = isJwtDecoded || isJwtEncoded ? 'jwt_vp' : ('ldp_vp' as const)
let vp: OriginalVerifiablePresentation
if (isJwtEncoded || isJwtDecoded) {
vp = CredentialMapper.jwtDecodedPresentationToUniformPresentation(decoded as JwtDecodedVerifiablePresentation, false, opts)
} else {
vp = decoded as IVerifiablePresentation
}
if (!vp) {
throw Error(`VP key not found`)
}
const noVCs = !('verifiableCredential' in vp) || !vp.verifiableCredential || vp.verifiableCredential.length === 0
if (noVCs) {
console.warn(`Presentation without verifiable credentials. That is rare! `)
// throw Error(`VP needs to have at least one verifiable credential at this point`)
}
const vcs = noVCs
? []
: (CredentialMapper.toWrappedVerifiableCredentials(
vp.verifiableCredential ?? [] /*.map(value => value.original)*/,
opts,
) as WrappedW3CVerifiableCredential[])
const presentation = {
...vp,
verifiableCredential: vcs, // We overwrite the verifiableCredentials with wrapped versions, making it an InternalVerifiablePresentation. Note: we keep the singular key name of the vc data model
} as UniformVerifiablePresentation
return {
type,
format,
original,
decoded,
presentation,
vcs,
}
}
/**
* Converts a list of credentials to a list of wrapped credentials.
*
* When decoding SD-JWT credentials, a hasher implementation must be provided. The hasher implementation must be sync. When using
* an async hasher implementation, use the decodeSdJwtVcAsync method instead and you can provide the decoded payload to methods
* instead of the compact SD-JWT.
*
* @param hasher Hasher implementation to use for SD-JWT decoding
*/
static toWrappedVerifiableCredentials(
verifiableCredentials: OriginalVerifiableCredential[],
opts?: { maxTimeSkewInMS?: number; hasher?: HasherSync },
): WrappedVerifiableCredential[] {
return verifiableCredentials.map((vc) => CredentialMapper.toWrappedVerifiableCredential(vc, opts))
}
/**
* Converts a credential to a wrapped credential.
*
* When decoding SD-JWT credentials, a hasher implementation must be provided. The hasher implementation must be sync. When using
* an async hasher implementation, use the decodeSdJwtVcAsync method instead and you can provide the decoded payload to methods
* instead of the compact SD-JWT.
*
* @param hasher Hasher implementation to use for SD-JWT decoding
*/
static toWrappedVerifiableCredential(
verifiableCredential: OriginalVerifiableCredential,
opts?: { maxTimeSkewInMS?: number; hasher?: HasherSync },
): WrappedVerifiableCredential {
// MSO_MDOC
if (CredentialMapper.isMsoMdocDecodedCredential(verifiableCredential) || CredentialMapper.isMsoMdocOid4VPEncoded(verifiableCredential)) {
let mdoc: MdocDocument
if (CredentialMapper.isMsoMdocOid4VPEncoded(verifiableCredential)) {
mdoc = decodeMdocIssuerSigned(verifiableCredential)
} else {
mdoc = verifiableCredential
}
return {
type: CredentialMapper.isMsoMdocDecodedCredential(verifiableCredential) ? OriginalType.MSO_MDOC_DECODED : OriginalType.MSO_MDOC_ENCODED,
format: 'mso_mdoc',
original: verifiableCredential,
credential: mdoc,
decoded: getMdocDecodedPayload(mdoc),
}
}
// SD-JWT
if (CredentialMapper.isSdJwtDecodedCredential(verifiableCredential) || CredentialMapper.isSdJwtEncoded(verifiableCredential)) {
let decodedCredential: SdJwtDecodedVerifiableCredential
if (CredentialMapper.isSdJwtEncoded(verifiableCredential)) {
const hasher = opts?.hasher ?? sha256
decodedCredential = decodeSdJwtVc(verifiableCredential, hasher)
} else {
decodedCredential = verifiableCredential
}
return {
type: CredentialMapper.isSdJwtDecodedCredential(verifiableCredential) ? OriginalType.SD_JWT_VC_DECODED : OriginalType.SD_JWT_VC_ENCODED,
format: 'vc+sd-jwt',
original: verifiableCredential,
credential: decodedCredential,
decoded: decodedCredential.decodedPayload,
}
}
// If the VC is not an encoded/decoded SD-JWT, we assume it will be a W3C VC
const proof = CredentialMapper.getFirstProof(verifiableCredential)
const original = CredentialMapper.hasJWTProofType(verifiableCredential) && proof ? (proof.jwt ?? verifiableCredential) : verifiableCredential
if (!original) {
throw Error(
'Could not determine original credential, probably it was a converted JWT credential, that is now missing the JWT value in the proof',
)
}
const decoded = CredentialMapper.decodeVerifiableCredential(original) as JwtDecodedVerifiableCredential | IVerifiableCredential
const isJwtEncoded = CredentialMapper.isJwtEncoded(original)
const isJwtDecoded = CredentialMapper.isJwtDecodedCredential(original)
const type = isJwtEncoded ? OriginalType.JWT_ENCODED : isJwtDecoded ? OriginalType.JWT_DECODED : OriginalType.JSONLD
const credential =
isJwtEncoded || isJwtDecoded
? CredentialMapper.jwtDecodedCredentialToUniformCredential(decoded as JwtDecodedVerifiableCredential, opts)
: (decoded as IVerifiableCredential)
const format = isJwtEncoded || isJwtDecoded ? ('jwt_vc' as const) : ('ldp_vc' as const)
return {
original,
decoded,
format,
type,
credential,
}
}
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 isMsoMdocOid4VPEncoded(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is string {
return ObjectUtils.isString(original) && !original.startsWith('ey') && ObjectUtils.isBase64(original)
}
public static isW3cCredential(credential: ICredential | SdJwtDecodedVerifiableCredential | MdocDocument): credential is ICredential {
return typeof credential === 'object' && '@context' in credential && ((credential as ICredential).type?.includes('VerifiableCredential') || false)
}
public static isCredential(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is OriginalVerifiableCredential {
try {
if (CredentialMapper.isJwtEncoded(original)) {
const vc: IVerifiableCredential = CredentialMapper.toUniformCredential(original)
return CredentialMapper.isW3cCredential(vc)
} else if (CredentialMapper.isSdJwtEncoded(original)) {
return true
} else if (CredentialMapper.isMsoMdocDecodedCredential(original)) {
return true
} else if (CredentialMapper.isMsoMdocOid4VPEncoded(original)) {
return true
}
return (
CredentialMapper.isW3cCredential(original as ICredential) ||
CredentialMapper.isSdJwtDecodedCredentialPayload(original as ICredential) ||
CredentialMapper.isJwtDecodedCredential(original as OriginalVerifiableCredential) ||
CredentialMapper.isSdJwtDecodedCredential(original as OriginalVerifiableCredential)
)
} catch (e) {
return false
}
}
public static isPresentation(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is OriginalVerifiablePresentation {
try {
if (CredentialMapper.isJwtEncoded(original)) {
const vp: IVerifiablePresentation = CredentialMapper.toUniformPresentation(original)
return CredentialMapper.isW3cPresentation(vp)
} else if (CredentialMapper.isSdJwtEncoded(original)) {
return false
// @ts-expect-error
} else if (CredentialMapper.isMsoMdocDecodedPresentation(original)) {
return true
} else if (CredentialMapper.isMsoMdocOid4VPEncoded(original)) {
return true
}
return (
CredentialMapper.isW3cPresentation(original as IPresentation) ||
CredentialMapper.isSdJwtDecodedCredentialPayload(original as ICredential) ||
CredentialMapper.isJwtDecodedPresentation(original as OriginalVerifiablePresentation) ||
CredentialMapper.isSdJwtDecodedCredential(original as OriginalVerifiableCredential)
)
} catch (e) {
return false
}
}
public static hasProof(original: OriginalVerifiableCredential | OriginalVerifiablePresentation | string): boolean {
try {
if (CredentialMapper.isMsoMdocOid4VPEncoded(original)) {
return false
// @ts-ignore
} else if (CredentialMapper.isMsoMdocDecodedCredential(original) || CredentialMapper.isMsoMdocDecodedPresentation(original)) {
return true
} else if (CredentialMapper.isJwtEncoded(original) || CredentialMapper.isJwtDecodedCredential(original as OriginalVerifiableCredential)) {
return true
} else if (CredentialMapper.isSdJwtEncoded(original) || CredentialMapper.isSdJwtDecodedCredential(original)) {
//todo: we might want to revisit this
return true
}
if (typeof original !== 'object') {
return false
}
if ('vc' in (original as JwtDecodedVerifiableCredential) && (original as JwtDecodedVerifiableCredential).vc.proof) {
return true
}
if ('vp' in (original as JwtDecodedVerifiablePresentation) && (original as JwtDecodedVerifiablePresentation).vp.proof) {
return true
}
return !!(original as IVerifiableCredential | IVerifiablePresentation).proof
} catch (e) {
return false
}
}
public static isW3cPresentation(
presentation: UniformVerifiablePresentation | IPresentation | SdJwtDecodedVerifiableCredential | DeviceResponseCbor,
): presentation is IPresentation {
return (
typeof presentation === 'object' &&
'@context' in presentation &&
((presentation as IPresentation).type?.includes('VerifiablePresentation') || false)
)
}
public static isSdJwtDecodedCredentialPayload(
credential: ICredential | SdJwtDecodedVerifiableCredentialPayload,
): credential is SdJwtDecodedVerifiableCredentialPayload {
return typeof credential === 'object' && 'vct' in credential
}
public static areOriginalVerifiableCredentialsEqual(firstOriginal: OriginalVerifiableCredential, secondOriginal: OriginalVerifiableCredential) {
// String (e.g. encoded jwt or SD-JWT)
if (typeof firstOriginal === 'string' || typeof secondOriginal === 'string') {
return firstOriginal === secondOriginal
} else if (CredentialMapper.isMsoMdocDecodedCredential(firstOriginal) || CredentialMapper.isMsoMdocDecodedCredential(secondOriginal)) {
if (!CredentialMapper.isMsoMdocDecodedCredential(firstOriginal) || !CredentialMapper.isMsoMdocDecodedCredential(secondOriginal)) {
// We are doing this over here, as the rest of the logic around it would otherwise need to be adjusted substantially
return false
}
// FIXME: mdoc library fails on parsing the device signed, so for now we just check whether the issuerSigned
// is equal, then we have a good chance it is the same credential. Once device signed parsing is fixed in mdl
// library we can move the .equals() to the top-level object.
return firstOriginal.issuerSigned.equals(secondOriginal.issuerSigned)
} 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 isJsonLdAsString(original: OriginalVerifiableCredential | OriginalVerifiablePresentation): original is string {
return ObjectUtils.isString(original) && original.includes('@context')
}
public static isSdJwtDecodedCredential(
original: OriginalVerifiableCredential | OriginalVerifiablePresentation | ICredential | IPresentation,
): original is SdJwtDecodedVerifiableCredential {
return (
typeof original === 'object' &&
((<SdJwtDecodedVerifiableCredential>original).compactSdJwtVc !== undefined || (<SdJwtDecodedVerifiableCredential>original).kbJwt !== undefined)
)
}
public static isMsoMdocDecodedCredential(
original: OriginalVerifiableCredential | OriginalVerifiablePresentation | ICredential | IPresentation,
): original is MdocDocument {
return typeof original === 'object' && 'issuerSigned' in original && (<MdocDocument>original).issuerSigned !== undefined
}
public static isMsoMdocDecodedPresentation(original: OriginalVerifiablePresentation): original is MdocDeviceResponse {
return typeof original === 'object' && 'version' in original && (<MdocDeviceResponse>original).version !== undefined
}
public static isJwtDecodedCredential(original: OriginalVerifiableCredential): original is JwtDecodedVerifiableCredential {
return (
typeof original === 'object' &&
(<JwtDecodedVerifiableCredential>original).vc !== undefined &&
(<JwtDecodedVerifiableCredential>original).iss !== undefined
)
}
public static isJwtDecodedPresentation(original: OriginalVerifiablePresentation): original is JwtDecodedVerifiablePresentation {
return (
typeof original === 'object' &&
(<JwtDecodedVerifiablePresentation>original).vp !== undefined &&
(<JwtDecodedVerifiablePresentation>original).iss !== undefined
)
}
public static isWrappedSdJwtVerifiableCredential = isWrappedSdJwtVerifiableCredential
public static isWrappedSdJwtVerifiablePresentation = isWrappedSdJwtVerifiablePresentation
public static isWrappedW3CVerifiableCredential = isWrappedW3CVerifiableCredential
public static isWrappedW3CVerifiablePresentation = isWrappedW3CVerifiablePresentation
public static isWrappedMdocCredential = isWrappedMdocCredential
public static isWrappedMdocPresentation = isWrappedMdocPresentation
static jwtEncodedPresentationToUniformPresentation(
jwt: string,
makeCredentialsUniform: boolean = true,
opts?: { maxTimeSkewInMS?: number },
): IPresentation {
return CredentialMapper.jwtDecodedPresentationToUniformPresentation(jwtDecode(jwt), makeCredentialsUniform, opts)
}
static jwtDecodedPresentationToUniformPresentation(
decoded: JwtDecodedVerifiablePresentation,
makeCredentialsUniform: boolean = true,
opts?: { maxTimeSkewInMS?: number },
): IVerifiablePresentation {
const { iss, aud, jti, vp, ...rest } = decoded
const presentation: IVerifiablePresentation = {
...rest,
...vp,
}
if (makeCredentialsUniform) {
if (!vp.verifiableCredential) {
throw Error('Verifiable Presentation should have a verifiable credential at this point')
}
presentation.verifiableCredential = vp.verifiableCredential.map((vc) => CredentialMapper.toUniformCredential(vc, opts))
}
if (iss) {
const holder = presentation.holder
if (holder) {
if (holder !== iss) {
throw new Error(`Inconsistent holders between JWT claim (${iss}) and VC value (${holder})`)
}
}
presentation.holder = iss
}
if (aud) {
const verifier = presentation.verifier
if (verifier) {
if (verifier !== aud) {
throw new Error(`Inconsistent holders between JWT claim (${aud}) and VC value (${verifier})`)
}
}
presentation.verifier = aud
}
if (jti) {
const id = presentation.id
if (id && id !== jti) {
throw new Error(`Inconsistent VP ids between JWT claim (${jti}) and VP value (${id})`)
}
presentation.id = jti
}
return presentation
}
static toUniformCredential(
verifiableCredential: OriginalVerifiableCredential,
opts?: {
maxTimeSkewInMS?: number
hasher?: HasherSync
},
): IVerifiableCredential {
if (CredentialMapper.isMsoMdocDecodedCredential(verifiableCredential)) {
return mdocDecodedCredentialToUniformCredential(verifiableCredential)
}
if (CredentialMapper.isSdJwtDecodedCredential(verifiableCredential)) {
return sdJwtDecodedCredentialToUniformCredential(verifiableCredential, opts)
}
const original =
typeof verifiableCredential !== 'string' && CredentialMapper.hasJWTProofType(verifiableCredential)
? CredentialMapper.getFirstProof(verifiableCredential)?.jwt
: verifiableCredential
if (!original) {
throw Error(
'Could not determine original credential from passed in credential. Probably because a JWT proof type was present, but now is not available anymore',
)
}
const decoded = CredentialMapper.decodeVerifiableCredential(original, opts?.hasher ?? sha256)
const isJwtEncoded: boolean = CredentialMapper.isJwtEncoded(original)
const isJwtDecoded: boolean = CredentialMapper.isJwtDecodedCredential(original)
const isSdJwtEncoded = CredentialMapper.isSdJwtEncoded(original)
const isMdocEncoded = CredentialMapper.isMsoMdocOid4VPEncoded(original)
if (isSdJwtEncoded) {
return sdJwtDecodedCredentialToUniformCredential(decoded as SdJwtDecodedVerifiableCredential, opts)
} else if (isMdocEncoded) {
return mdocDecodedCredentialToUniformCredential(decodeMdocIssuerSigned(original))
} else if (isJwtDecoded || isJwtEncoded) {
return CredentialMapper.jwtDecodedCredentialToUniformCredential(decoded as JwtDecodedVerifiableCredential, opts)
} else {
return decoded as IVerifiableCredential
}
}
static toUniformPresentation(
presentation: OriginalVerifiablePresentation,
opts?: { maxTimeSkewInMS?: number; addContextIfMissing?: boolean; hasher?: HasherSync },
): IVerifiablePresentation {
if (CredentialMapper.isSdJwtDecodedCredential(presentation)) {
throw new Error('Converting SD-JWT VC to uniform VP is not supported.')
} else if (CredentialMapper.isMsoMdocDecodedPresentation(presentation)) {
throw new Error('Converting MSO_MDOC to uniform VP is not supported yet.')
}
const proof = CredentialMapper.getFirstProof(presentation)
const original = typeof presentation !== 'string' && CredentialMapper.hasJWTProofType(presentation) ? proof?.jwt : presentation
if (!original) {
throw Error(
'Could not determine original presentation, probably it was a converted JWT presentation, that is now missing the JWT value in the proof',
)
}
const decoded = CredentialMapper.decodeVerifiablePresentation(original, opts?.hasher ?? sha256)
const isJwtEncoded: boolean = CredentialMapper.isJwtEncoded(original)
const isJwtDecoded: boolean = CredentialMapper.isJwtDecodedPresentation(original)
const uniformPresentation =
isJwtEncoded || isJwtDecoded
? CredentialMapper.jwtDecodedPresentationToUniformPresentation(decoded as JwtDecodedVerifiablePresentation, false)
: (decoded as IVerifiablePresentation)
// At time of writing Velocity Networks does not conform to specification. Adding bare minimum @context section to stop parsers from crashing and whatnot
if (opts?.addContextIfMissing && !uniformPresentation['@context']) {
uniformPresentation['@context'] = ['https://www.w3.org/2018/credentials/v1']
}
uniformPresentation.verifiableCredential = uniformPresentation.verifiableCredential?.map((vc) =>
CredentialMapper.toUniformCredential(vc, opts),
) as IVerifiableCredential[] // We cast it because we IPresentation needs a VC. The internal Credential doesn't have the required Proof anymore (that is intended)
return uniformPresentation
}
static jwtEncodedCredentialToUniformCredential(
jwt: string,
opts?: {
maxTimeSkewInMS?: number
},
): IVerifiableCredential {
return CredentialMapper.jwtDecodedCredentialToUniformCredential(jwtDecode(jwt), opts)
}
static jwtDecodedCredentialToUniformCredential(
decoded: JwtDecodedVerifiableCredential,
opts?: { maxTimeSkewInMS?: number },
): IVerifiableCredential {
const { exp, nbf, iss, vc, sub, jti, ...rest } = decoded
const credential: IVerifiableCredential = {
...rest,
...vc,
}
const maxSkewInMS = opts?.maxTimeSkewInMS ?? 1500
if (exp) {
const expDate = credential.expirationDate
const jwtExp = parseInt(exp.toString())
// fix seconds to millisecond for the date
const expDateAsStr = jwtExp < 9999999999 ? new Date(jwtExp * 1000).toISOString().replace(/\.000Z/, 'Z') : new Date(jwtExp).toISOString()
if (expDate && expDate !== expDateAsStr) {
const diff = Math.abs(new Date(expDateAsStr).getTime() - new Date(expDate).getTime())
if (!maxSkewInMS || diff > maxSkewInMS) {
throw new Error(`Inconsistent expiration dates between JWT claim (${expDateAsStr}) and VC value (${expDate})`)
}
}
credential.expirationDate = expDateAsStr
}
if (nbf) {
const issuanceDate = credential.issuanceDate
const jwtNbf = parseInt(nbf.toString())
// fix seconds to millisecs for the date
const nbfDateAsStr = jwtNbf < 9999999999 ? new Date(jwtNbf * 1000).toISOString().replace(/\.000Z/, 'Z') : new Date(jwtNbf).toISOString()
if (issuanceDate && issuanceDate !== nbfDateAsStr) {
const diff = Math.abs(new Date(nbfDateAsStr).getTime() - new Date(issuanceDate).getTime())
if (!maxSkewInMS || diff > maxSkewInMS) {
throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`)
}
}
credential.issuanceDate = nbfDateAsStr
}
if (iss) {
const issuer = credential.issuer
if (issuer) {
if (typeof issuer === 'string') {
if (issuer !== iss) {
throw new Error(`Inconsistent issuers between JWT claim (${iss}) and VC value (${issuer})`)
}
} else {
if (!issuer.id && Object.keys(issuer).length > 0) {
// We have an issuer object with more than 1 property but without an issuer id. Set it,
// because the default behaviour of did-jwt-vc is to remove the id value when creating JWTs
issuer.id = iss
}
if (issuer.id !== iss) {
throw new Error(`Inconsistent issuers between JWT claim (${iss}) and VC value (${issuer.id})`)
}
}
} else {
credential.issuer = iss
}
}
if (sub) {
const subjects = Array.isArray(credential.credentialSubject) ? credential.credentialSubject : [credential.credentialSubject]
for (let i = 0; i < subjects.length; i++) {
const csId = subjects[i].id
if (csId && csId !== sub) {
throw new Error(`Inconsistent credential subject ids between JWT claim (${sub}) and VC value (${csId})`)
}
Array.isArray(credential.credentialSubject) ? (credential.credentialSubject[i].id = sub) : (credential.credentialSubject.id = sub)
}
}
if (jti) {
const id = credential.id
if (id && id !== jti) {
throw new Error(`Inconsistent credential ids between JWT claim (${jti}) and VC value (${id})`)
}
credential.id = jti
}
return credential
}
static toExternalVerifiableCredential(verifiableCredential: any): IVerifiableCredential {
let proof
if (verifiableCredential.proof) {
if (!verifiableCredential.proof.type) {
throw new Error('Verifiable credential proof is missing a type')
}
if (!verifiableCredential.proof.created) {
throw new Error('Verifiable credential proof is missing a created date')
}
if (!verifiableCredential.proof.proofPurpose) {
throw new Error('Verifiable credential proof is missing a proof purpose')
}
if (!verifiableCredential.proof.verificationMethod) {
throw new Error('Verifiable credential proof is missing a verification method')
}
proof = {
...verifiableCredential.proof,
type: verifiableCredential.proof.type,
created: verifiableCredential.proof.created,
proofPurpose: verifiableCredential.proof.proofPurpose,
verificationMethod: verifiableCredential.proof.verificationMethod,
}
}
return {
...verifiableCredential,
type: verifiableCredential.type
? typeof verifiableCredential.type === 'string'
? [verifiableCredential.type]
: verifiableCredential.type
: ['VerifiableCredential'],
proof,
}
}
static storedCredentialToOriginalFormat(credential: OriginalVerifiableCredential): W3CVerifiableCredential {
const type: DocumentFormat = CredentialMapper.detectDocumentType(credential)
if (typeof credential === 'string') {
if (type === DocumentFormat.JWT) {
return CredentialMapper.toCompactJWT(credential)
} else if (type === DocumentFormat.JSONLD) {
return JSON.parse(credential)
} else if (type === DocumentFormat.SD_JWT_VC) {
return credential
}
} else if (type === DocumentFormat.JWT && ObjectUtils.isObject(credential) && 'vc' in credential) {
return CredentialMapper.toCompactJWT(credential)
} else if (ObjectUtils.isObject(credential) && 'proof' in credential && credential.proof.type === 'JwtProof2020' && credential.proof.jwt) {
return credential.proof.jwt
} else if (
ObjectUtils.isObject(credential) &&
'proof' in credential &&
credential.proof.type === IProofType.SdJwtProof2024 &&
credential.proof.jwt
) {
return credential.proof.jwt
} else if (type === DocumentFormat.SD_JWT_VC && this.isSdJwtDecodedCredential(credential)) {
return credential.compactSdJwtVc
}
return credential as W3CVerifiableCredential
}
static storedPresentationToOriginalFormat(presentation: OriginalVerifiablePresentation): W3CVerifiablePresentation {
const type: DocumentFormat = CredentialMapper.detectDocumentType(presentation)
if (typeof presentation === 'string') {
if (type === DocumentFormat.JWT) {
return CredentialMapper.toCompactJWT(presentation)
} else if (type === DocumentFormat.JSONLD) {
return JSON.parse(presentation)
}
} else if (type === DocumentFormat.JWT && ObjectUtils.isObject(presentation) && 'vp' in presentation) {
return CredentialMapper.toCompactJWT(presentation)
} else if (
ObjectUtils.isObject(presentation) &&
'proof' in presentation &&
presentation.proof.type === 'JwtProof2020' &&
presentation.proof.jwt
) {
return presentation.proof.jwt
}
return presentation as W3CVerifiablePresentation
}
static toCompactJWT(
jwtDocument: W3CVerifiableCredential | JwtDecodedVerifiableCredential | W3CVerifiablePresentation | JwtDecodedVerifiablePresentation | string,
): string {
if (!jwtDocument || CredentialMapper.detectDocumentType(jwtDocument) !== DocumentFormat.JWT) {
throw Error('Cannot convert non JWT credential to JWT')
}
if (typeof jwtDocument === 'string') {
return jwtDocument
}
let proof: string | undefined
if (ObjectUtils.isObject(jwtDocument) && 'vp' in jwtDocument) {
proof = 'jwt' in jwtDocument.vp.proof ? jwtDocument.vp.proof.jwt : jwtDocument.vp.proof
} else if (ObjectUtils.isObject(jwtDocument) && 'vc' in jwtDocument) {
proof = 'jwt' in jwtDocument.vc.proof ? jwtDocument.vc.proof.jwt : jwtDocument.vc.proof
} else {
proof = Array.isArray(jwtDocument.proof) ? jwtDocument.proof[0].jwt : jwtDocument.proof.jwt
}
if (!proof) {
throw Error(`Could not get JWT from supplied document`)
}
return proof
}
static detectDocumentType(
document:
| W3CVerifiableCredential
| W3CVerifiablePresentation
| JwtDecodedVerifiableCredential
| JwtDecodedVerifiablePresentation
| SdJwtDecodedVerifiableCredential
| MdocDeviceResponse
| MdocDocument,
): DocumentFormat {
if (this.isMsoMdocOid4VPEncoded(document as any) || this.isMsoMdocDecodedCredential(document as any)) {
return DocumentFormat.MSO_MDOC
} else if (this.isMsoMdocDecodedPresentation(document as any) || this.isMsoMdocDecodedPresentation(document as any)) {
return DocumentFormat.MSO_MDOC
} else 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 =
typeof document !== 'string' &&
('vc' in document ? document.vc.proof : 'vp' in document ? document.vp.proof : (<IVerifiableCredential>document).proof)
const proof: IProof = Array.isArray(proofs) ? proofs[0] : proofs
if (proof?.jwt) {
return DocumentFormat.JWT
} else if (proof?.type === 'EthereumEip712Signature2021') {
return DocumentFormat.EIP712
}
return DocumentFormat.JSONLD
}
private static hasJWTProofType(
document: W3CVerifiableCredential | W3CVerifiablePresentation | JwtDecodedVerifiableCredential | JwtDecodedVerifiablePresentation,
): boolean {
if (typeof document === 'string') {
return false
}
return !!CredentialMapper.getFirstProof(document)?.jwt
}
private static getFirstProof(
document: W3CVerifiableCredential | W3CVerifiablePresentation | JwtDecodedVerifiableCredential | JwtDecodedVerifiablePresentation,
): IProof | undefined {
if (!document || typeof document === 'string') {
return undefined
}
const proofs = 'vc' in document ? document.vc.proof : 'vp' in document ? document.vp.proof : (<IVerifiableCredential>document).proof
return Array.isArray(proofs) ? proofs[0] : proofs
}
static issuerCorrelationIdFromIssuerType(issuer: IssuerType): string {
if (issuer === undefined) {
throw new Error('Issuer type us undefined')
} else if (typeof issuer === 'string') {
return issuer
} else if (typeof issuer === 'object') {
if ('id' in issuer) {
return issuer.id
} else {
throw new Error('Encountered an invalid issuer object: missing id property')
}
} else {
throw new Error('Invalid issuer type')
}
}
}