Skip to content

Commit

Permalink
feat(sd-jwt): create sd-jwt as an issuer
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <blu3beri@proton.me>
  • Loading branch information
berendsliedrecht committed Oct 17, 2023
1 parent 594ccac commit 17e235e
Show file tree
Hide file tree
Showing 13 changed files with 432 additions and 17 deletions.
7 changes: 1 addition & 6 deletions packages/core/src/crypto/jose/jwk/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
export {
getJwkFromJson,
getJwkFromKey,
getJwkClassFromJwaSignatureAlgorithm,
getJwkClassFromKeyType,
} from './transform'
export * from './transform'
export { Ed25519Jwk } from './Ed25519Jwk'
export { X25519Jwk } from './X25519Jwk'
export { P256Jwk } from './P256Jwk'
Expand Down
20 changes: 17 additions & 3 deletions packages/core/src/crypto/jose/jwk/transform.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { JwkJson, Jwk } from './Jwk'
import type { Key } from '../../Key'
import type { JwaSignatureAlgorithm } from '../jwa'

import { AriesFrameworkError } from '../../../error'
import { KeyType } from '../../KeyType'
import { JwaCurve, JwaKeyType } from '../jwa'
import { JwaSignatureAlgorithm, JwaCurve, JwaKeyType } from '../jwa'

import { Ed25519Jwk } from './Ed25519Jwk'
import { P256Jwk } from './P256Jwk'
Expand Down Expand Up @@ -37,7 +37,7 @@ export function getJwkFromKey(key: Key) {
if (key.keyType === KeyType.P384) return P384Jwk.fromPublicKey(key.publicKey)
if (key.keyType === KeyType.P521) return P521Jwk.fromPublicKey(key.publicKey)

throw new Error(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`)
throw new AriesFrameworkError(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`)
}

export function getJwkClassFromJwaSignatureAlgorithm(alg: JwaSignatureAlgorithm | string) {
Expand All @@ -47,3 +47,17 @@ export function getJwkClassFromJwaSignatureAlgorithm(alg: JwaSignatureAlgorithm
export function getJwkClassFromKeyType(keyType: KeyType) {
return JwkClasses.find((jwkClass) => jwkClass.keyType === keyType)
}

/**
* Get a JSON Web Algorithm (JWA) from a key type.
*
* if it cannot be detected, it will throw an error
*/
export function getJwaFromKeyType(keyType: KeyType): JwaSignatureAlgorithm {
if (keyType === KeyType.Ed25519) return JwaSignatureAlgorithm.EdDSA
if (keyType === KeyType.P256) return JwaSignatureAlgorithm.ES256
if (keyType === KeyType.P384) return JwaSignatureAlgorithm.ES384
if (keyType === KeyType.P521) return JwaSignatureAlgorithm.ES512

throw new AriesFrameworkError(`Cannot create JWA from key type. Unsupported key type '${keyType}'.`)
}
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export * from './crypto/'

// TODO: clean up util exports
export { encodeAttachment, isLinkedAttachment } from './utils/attachment'
export { Hasher } from './utils/Hasher'
export { Hasher, HashName } from './utils/Hasher'
export { MessageValidator } from './utils/MessageValidator'
export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment'
import { parseInvitationUrl } from './utils/parseInvitation'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { RecordTags, TagsBase } from '../../../storage/BaseRecord'
import type { TagsBase } from '../../../storage/BaseRecord'

import { BaseRecord } from '../../../storage/BaseRecord'
import { uuid } from '../../../utils/uuid'

export type GenericRecordTags = TagsBase

export type BasicMessageTags = RecordTags<GenericRecord>

export interface GenericRecordStorageProps {
id?: string
createdAt?: Date
Expand Down
9 changes: 7 additions & 2 deletions packages/sd-jwt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"main": "build/index",
"types": "build/index",
"version": "0.4.1",
"files": ["build"],
"files": [
"build"
],
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
Expand All @@ -22,11 +24,14 @@
"test": "jest"
},
"dependencies": {
"@aries-framework/askar": "^0.4.2",
"@aries-framework/core": "0.4.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.0"
"class-validator": "0.14.0",
"jwt-sd": "^0.0.1-alpha.13"
},
"devDependencies": {
"@hyperledger/aries-askar-nodejs": "^0.1.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^4.4.0",
"typescript": "~4.9.5"
Expand Down
49 changes: 49 additions & 0 deletions packages/sd-jwt/src/SdJwtApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import type {
SdJwt,
SdJwtCreateOptions,
SdJwtPresentOptions,
SdJwtReceiveOptions,
SdJwtVerificationResult,
SdJwtVerifyOptions,
} from './SdJwtService'
import type { SdJwtRecord } from './repository'
import type { Jwt } from '@aries-framework/core'

import { AgentContext, injectable } from '@aries-framework/core'

import { SdJwtService } from './SdJwtService'
Expand All @@ -14,4 +25,42 @@ export class SdJwtApi {
this.agentContext = agentContext
this.sdJwtService = sdJwtService
}

/**
* Taking a JWT here is a really suboptimal. It would be nice to also be able to take something like a VC, or something like that.
*/
public async create(jwt: Jwt, options: SdJwtCreateOptions): Promise<SdJwtRecord> {
return await this.sdJwtService.create(this.agentContext, jwt, options)
}

/**
* @todo Name is not the best
* @todo This stores for now as it is in-line with the rest of the framework, but this will be removed.
*
* Validates and stores an sd-jwt from the perspective of an holder
*/
public async receive(sdJwt: SdJwt, options: SdJwtReceiveOptions): Promise<SdJwtRecord> {
return await this.sdJwtService.receive(this.agentContext, sdJwt, options)
}

/**
* Create a compact presentation of the sd-jwt.
* This presentation can be send in- or out-of-band to the verifier.
*
* Within the `options` field, you can supply the indicies of the disclosures you would like to share with the verifier.
* Also, whether to include the holder key binding.
*
*/
public async present(sdJwt: SdJwt, options: SdJwtPresentOptions): Promise<string> {
return await this.sdJwtService.present(this.agentContext, sdJwt, options)
}

/**
* Verify an incoming sd-jwt. It will check whether everything is valid, but also returns parts of the validation.
*
* For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid.
*/
public async verify(sdJwt: SdJwt | string, options: SdJwtVerifyOptions): Promise<SdJwtVerificationResult> {
return await this.sdJwtService.verify(this.agentContext, sdJwt, options)
}
}
7 changes: 7 additions & 0 deletions packages/sd-jwt/src/SdJwtError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { AriesFrameworkError } from '@aries-framework/core'

export class SdJwtError extends AriesFrameworkError {
public constructor(message: string, { cause }: { cause?: Error } = {}) {
super(message, { cause })
}
}
168 changes: 167 additions & 1 deletion packages/sd-jwt/src/SdJwtService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,57 @@
import { inject, injectable, InjectionSymbols, Logger } from '@aries-framework/core'
import type { AgentContext, HashName, Key } from '@aries-framework/core'
import type { Signer } from 'jwt-sd'
import type { DisclosureFrame, Verifier } from 'jwt-sd/build/sdJwt/types'

import {
getJwkFromKey,
Jwt,
Hasher,
inject,
injectable,
InjectionSymbols,
Logger,
TypedArrayEncoder,
Buffer,
getJwaFromKeyType,
} from '@aries-framework/core'
import { HasherAlgorithm, SdJwt } from 'jwt-sd'

export { SdJwt }

import { SdJwtError } from './SdJwtError'
import { SdJwtRecord } from './repository/SdJwtRecord'

export type SdJwtCreateOptions<Payload extends Record<string, unknown> = Record<string, unknown>> = {
disclosureFrame?: DisclosureFrame<Payload>
issuerKey: Key
holderKey?: Key
hashingAlgorithm?: HashName
}

export type SdJwtReceiveOptions = {
issuerKey: Key
holderKey?: Key
}

export type SdJwtPresentOptions = {
includedDisclosureIndices?: Array<number>
includeHolderKey?: boolean
}

/**
* @todo combine requiredClaims and requiredDisclosedClaims
*/
export type SdJwtVerifyOptions = {
requiredClaims?: Array<string>
holderKey?: Key
}

export type SdJwtVerificationResult = {
isValid: boolean
isSignatureValid: boolean
areRequiredClaimsIncluded?: boolean
areDisclosedClaimsIncluded?: boolean
}

/**
* @internal
Expand All @@ -10,4 +63,117 @@ export class SdJwtService {
public constructor(@inject(InjectionSymbols.Logger) logger: Logger) {
this.logger = logger
}

private hasher(input: string) {
const serializedInput = TypedArrayEncoder.fromString(input)
const hash = Hasher.hash(serializedInput, 'sha2-256')

return TypedArrayEncoder.toBase64URL(hash)
}

/**
* @todo validate the JWT header (alg)
*/
private signer<Header extends Record<string, unknown> = Record<string, unknown>>(
agentContext: AgentContext,
key: Key
): Signer<Header> {
return async (input: string) => agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) })
}

/**
* @todo validate the JWT header (alg)
*/
private verifier<Header extends Record<string, unknown> = Record<string, unknown>>(
agentContext: AgentContext,
issuerKey: Key
): Verifier<Header> {
return async ({ message, signature }) => {
return await agentContext.wallet.verify({
signature: Buffer.from(signature),
key: issuerKey,
data: TypedArrayEncoder.fromString(message),
})
}
}

public async create<Payload extends Record<string, unknown> = Record<string, unknown>>(
agentContext: AgentContext,
jwt: Jwt | Payload,
{ issuerKey, disclosureFrame, hashingAlgorithm = 'sha2-256', holderKey }: SdJwtCreateOptions<Payload>
): Promise<SdJwtRecord> {
if (hashingAlgorithm !== 'sha2-256') {
throw new SdJwtError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`)
}

const { header, payload } =
jwt instanceof Jwt
? { header: jwt.header, payload: jwt.payload.toJson() }
: {
header: { alg: getJwaFromKeyType(issuerKey.keyType).toString() },
payload: jwt,
}

const confirmationClaim = holderKey ? getJwkFromKey(holderKey).toJson() : undefined

let sdJwt = new SdJwt<typeof header, typeof payload>()
.withHeader(header)
.withPayload(payload)
.withHasher({ hasher: this.hasher, algorithm: HasherAlgorithm.Sha256 })
.withSigner(this.signer(agentContext, issuerKey))
.withSaltGenerator(agentContext.wallet.generateNonce)

sdJwt = confirmationClaim ? sdJwt.addPayloadClaim('cnf', confirmationClaim) : sdJwt
sdJwt = disclosureFrame ? sdJwt.withDisclosureFrame(disclosureFrame) : sdJwt

const compact = await sdJwt.toCompact()

return new SdJwtRecord({
sdJwt: compact,
})
}

/**
* @todo Name is not the best
* @todo fix with the newer API
*/
public async receive(
agentContext: AgentContext,
sdJwt: SdJwt,
{ holderKey, issuerKey }: SdJwtReceiveOptions
): Promise<SdJwtRecord> {
const isValid = await sdJwt.verifySignature(this.verifier(agentContext, issuerKey))

if (!isValid) {
throw new SdJwtError(`sd-jwt is not valid.`)
}

// TODO: append holder key here
const compact = await sdJwt.toCompact()

return new SdJwtRecord({
sdJwt: compact,
})
}

public async present(
agentContext: AgentContext,
sdJwt: SdJwt,
{ includedDisclosureIndices }: SdJwtPresentOptions
): Promise<string> {
return 'header.payload.signature~disclosure_0~disclosure_1~key_binding'
}

public async verify(
agentContext: AgentContext,
sdJwt: SdJwt | string,
{ holderKey, requiredClaims }: SdJwtVerifyOptions
): Promise<SdJwtVerificationResult> {
return {
isValid: true,
isSignatureValid: true,
areRequiredClaimsIncluded: true,
areDisclosedClaimsIncluded: true,
}
}
}
Loading

0 comments on commit 17e235e

Please sign in to comment.