@sphereon/ssi-sdk-ext.jwt-service
Version:
328 lines (269 loc) • 9.35 kB
text/typescript
import {
ExternalIdentifierDidOpts,
ExternalIdentifierResult,
ExternalIdentifierX5cOpts,
IIdentifierResolution,
ManagedIdentifierOptsOrResult,
ManagedIdentifierResult,
} from '@sphereon/ssi-sdk-ext.identifier-resolution'
import { ClientIdScheme } from '@sphereon/ssi-sdk-ext.x509-utils'
import { BaseJWK, IValidationResult, JoseSignatureAlgorithm, JoseSignatureAlgorithmString, JWK } from '@sphereon/ssi-types'
import { IAgentContext, IKeyManager, IPluginMethodMap } from '@veramo/core'
export type IRequiredContext = IAgentContext<IIdentifierResolution & IKeyManager> // could we still interop with Veramo?
export const jwtServiceContextMethods: Array<string> = [
'jwtPrepareJws',
'jwtCreateJwsJsonGeneralSignature',
'jwtCreateJwsJsonFlattenedSignature',
'jwtCreateJwsCompactSignature',
'jwtVerifyJwsSignature',
'jwtEncryptJweCompactJwt',
'jwtDecryptJweCompactJwt',
]
export interface IJwtService extends IPluginMethodMap {
jwtPrepareJws(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<PreparedJwsObject>
jwtCreateJwsJsonGeneralSignature(args: CreateJwsJsonArgs, context: IRequiredContext): Promise<JwsJsonGeneral>
jwtCreateJwsJsonFlattenedSignature(args: CreateJwsFlattenedArgs, context: IRequiredContext): Promise<JwsJsonFlattened>
jwtCreateJwsCompactSignature(args: CreateJwsCompactArgs, context: IRequiredContext): Promise<JwtCompactResult>
jwtVerifyJwsSignature(args: VerifyJwsArgs, context: IRequiredContext): Promise<IJwsValidationResult>
jwtEncryptJweCompactJwt(args: EncryptJweCompactJwtArgs, context: IRequiredContext): Promise<JwtCompactResult>
jwtDecryptJweCompactJwt(args: DecryptJweCompactJwtArgs, context: IRequiredContext): Promise<JwtCompactResult>
// TODO: JWE/encryption general methods
}
export type IJwsValidationResult = IValidationResult & {
jws: JwsJsonGeneralWithIdentifiers // We always translate to general as that is the most flexible format allowing multiple sigs
}
export interface PreparedJws {
protectedHeader: JwsHeader
payload: Uint8Array
unprotectedHeader?: JwsHeader // only for jws json and also then optional
existingSignatures?: Array<JwsJsonSignature> // only for jws json and also then optional
}
export interface JwsJsonSignature {
protected: string
header?: JwsHeader
signature: string
}
/**
* The JWK representation of an ephemeral public key.
* See https://www.rfc-editor.org/rfc/rfc7518.html#section-6
*/
// todo split into separate objects
export type EphemeralPublicKey = Omit<BaseJWK, 'alg'>
// export function isEcJWK(v)
export interface JweHeader extends Omit<BaseJwtHeader, 'alg'> {
alg: string
enc: string
jku?: string
jwk?: BaseJWK
epk?: EphemeralPublicKey
x5u?: string
x5c?: string[]
x5t?: string
cty?: string
crit?: string[]
[k: string]: any
}
export interface JweRecipientUnprotectedHeader {
alg: string
iv: string
tag: string
epk?: EphemeralPublicKey
kid?: string
apv?: string
apu?: string
}
export interface JweProtectedHeader extends Partial<JweHeader> {
zip?: 'DEF' | string
}
export type Jws = JwsCompact | JwsJsonFlattened | JwsJsonGeneral
export type JwsCompact = string
export interface JwsJsonFlattened {
payload: string
protected: string
header?: JwsHeader
signature: string
}
export interface JwsJsonGeneral {
payload: string
signatures: Array<JwsJsonSignature>
}
export interface JwsJsonGeneralWithIdentifiers extends JwsJsonGeneral {
signatures: Array<JwsJsonSignatureWithIdentifier>
}
export interface JwsJsonSignatureWithIdentifier extends JwsJsonSignature {
identifier: ExternalIdentifierResult
}
export type Jwe = JweCompact | JweJsonFlattened | JweJsonGeneral
export type JweCompact = string
export interface JweJsonFlattened {
protected: string
unprotected: JweHeader
header: JweHeader | JweRecipientUnprotectedHeader
encrypted_key?: string
aad?: string
iv: string
ciphertext: string
tag?: string
}
export interface JweRecipient {
header?: JweRecipientUnprotectedHeader
encrypted_key?: string
}
export interface JweJsonGeneral {
protected: string
unprotected?: JweHeader
recipients: Array<JweRecipient>
aad?: string
iv: string
ciphertext: string
tag?: string
}
export interface PreparedJwsObject {
jws: PreparedJws
b64: { payload: string; protectedHeader: string } // header is always json, as it can only be used in JwsJson
identifier: ManagedIdentifierResult
}
export interface BaseJwtHeader {
typ?: string
alg?: string
kid?: string
}
export interface BaseJwtPayload {
iss?: string
sub?: string
aud?: string[] | string
exp?: number
nbf?: number
iat?: number
jti?: string
}
export interface JwsHeader extends BaseJwtHeader {
kid?: string
jwk?: JWK
x5c?: string[]
[key: string]: unknown
}
export interface JwsPayload extends BaseJwtPayload {
[key: string]: unknown
}
export interface JwsHeaderOpts {
alg: JoseSignatureAlgorithm | JoseSignatureAlgorithmString
}
export type JwsIdentifierMode = 'x5c' | 'kid' | 'jwk' | 'did' | 'auto'
export type EncryptJweCompactJwtArgs = {
payload: JwsPayload
protectedHeader?: JweProtectedHeader | undefined
aad?: Uint8Array | undefined
recipientKey: ExternalIdentifierResult & { kid?: string }
alg?: JweAlg
enc?: JweEnc
apu?: string // base64url
apv?: string // base64url
expirationTime?: number | string | Date
issuer?: string
audience?: string | string[]
}
export type DecryptJweCompactJwtArgs = {
jwe: JweCompact
idOpts: ManagedIdentifierOptsOrResult
}
export type CreateJwsArgs = {
mode?: JwsIdentifierMode
issuer: ManagedIdentifierOptsOrResult & {
noIssPayloadUpdate?: boolean
noIdentifierInHeader?: boolean
}
clientId?: string
clientIdScheme?: ClientIdScheme | 'did' | string
protectedHeader: JwsHeader
payload: JwsPayload | Uint8Array | string
}
export type CreateJweArgs = {
mode?: JwsIdentifierMode
issuer: ManagedIdentifierOptsOrResult & {
noIssPayloadUpdate?: boolean
noIdentifierInHeader?: boolean
}
protectedHeader: JweProtectedHeader
encryptedKey: string | EphemeralPublicKey // In case it is a string it is already encrypted; otherwise encrypt //TODO ??
iv: string
ciphertext: string
tag: string
}
export type CreateJwsCompactArgs = CreateJwsArgs
export type CreateJwsFlattenedArgs = Exclude<CreateJwsJsonArgs, 'existingSignatures'>
export type VerifyJwsArgs = {
jws: Jws
jwk?: JWK // Jwk will be resolved from jws, but you can also provide one
opts?: { x5c?: Omit<ExternalIdentifierX5cOpts, 'identifier'>; did?: Omit<ExternalIdentifierDidOpts, 'identifier'> }
}
/**
* @public
*/
export type CreateJwsJsonArgs = CreateJwsArgs & {
unprotectedHeader?: JwsHeader // only for jws json
existingSignatures?: Array<JwsJsonSignature> // Only for jws json
}
export type CreateJweJsonArgs = CreateJweArgs & {
unprotectedHeader?: JweHeader
}
/**
* @public
*/
export interface JwtCompactResult {
jwt: JwsCompact | JweCompact
}
export function isJwsCompact(jws: Jws): jws is JwsCompact {
return typeof jws === 'string' && jws.split('~')[0].match(COMPACT_JWS_REGEX) !== null
}
export function isJweCompact(jwe: Jwe): jwe is JweCompact {
return typeof jwe === 'string' && jwe.split('~')[0].match(COMPACT_JWE_REGEX) !== null
}
export function isJwsJsonFlattened(jws: Jws): jws is JwsJsonFlattened {
return typeof jws === 'object' && 'signature' in jws && 'protected' in jws && !('ciphertext' in jws)
}
export function isJwsJsonGeneral(jws: Jws): jws is JwsJsonGeneral {
return typeof jws === 'object' && 'signatures' in jws && !('ciphertext' in jws)
}
export function isJweJsonFlattened(jwe: Jwe): jwe is JweJsonFlattened {
return typeof jwe === 'object' && 'signature' in jwe && 'ciphertext' in jwe && !('payload' in jwe)
}
export function isJweJsonGeneral(jwe: Jwe): jwe is JweJsonGeneral {
return typeof jwe === 'object' && 'signatures' in jwe && 'ciphertext' in jwe && !('payload' in jwe)
}
export function isJwsHeader(header: BaseJwtHeader & Record<string, any>): header is JwsHeader {
return header && !isJweHeader(header)
}
export function isJweHeader(header: BaseJwtHeader & Record<string, any>): header is JweHeader {
return ('enc' in header && header.enc && jweEnc(header.enc)) || (header.alg && jweAlg(header.alg))
}
export const COMPACT_JWS_REGEX = /^([a-zA-Z0-9_=-]+).([a-zA-Z0-9_=-]+)?.([a-zA-Z0-9_=-]+)?$/
export const COMPACT_JWE_REGEX = /^([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)\.([a-zA-Z0-9_=-]+)?\.([a-zA-Z0-9_=-]+)?$/
export const JweAlgs = [
'RSA1_5',
'RSA-OAEP',
'RSA-OAEP-256',
'A128KW',
'A192KW',
'A256KW',
'dir',
'ECDH-ES' /*interop value*/,
'ECDH-ES+A128KW',
'ECDH-ES+A192KW',
'ECDH-ES+A256KW',
'A128GCMKW',
'A192GCMKW',
'A256GCMKW',
'PBES2-HS256+A128KW',
'PBES2-HS384+A192KW',
'PBES2-HS512+A256KW',
] as const
export type JweAlg = (typeof JweAlgs)[number]
export function jweAlg(alg?: string | JweAlg): JweAlg | undefined {
return JweAlgs.find((supportedVal) => supportedVal === alg)
}
export const JweEncs = ['A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512', 'A128GCM', 'A192GCM', 'A256GCM' /*interop value*/] as const
export type JweEnc = (typeof JweEncs)[number]
export function jweEnc(alg?: string | JweEnc): JweEnc | undefined {
return JweEncs.find((supportedVal) => supportedVal === alg)
}