Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 15 additions & 139 deletions .env.demo

Large diffs are not rendered by default.

154 changes: 13 additions & 141 deletions .env.sample

Large diffs are not rendered by default.

38 changes: 31 additions & 7 deletions src/cliAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
InMemoryLruCache,
WebDidResolver,
LogLevel,
Agent
Agent,
X509Module
} from '@credo-ts/core'
import {
HttpOutboundTransport,
Expand Down Expand Up @@ -64,7 +65,7 @@ import { generateSecretKey } from './utils/helpers'
import { TsLogger } from './utils/logger'
import { OpenId4VcHolderModule, OpenId4VcIssuerModule, OpenId4VcVerifierModule } from '@credo-ts/openid4vc'
import { Router } from 'express'
import { getCredentialRequestToCredentialMapper } from './utils/oid4vc-agent'
import { getCredentialRequestToCredentialMapper, getMixedCredentialRequestToCredentialMapper } from './utils/oid4vc-agent'

const openId4VciRouter = Router()
const openId4VpRouter = Router()
Expand Down Expand Up @@ -201,7 +202,7 @@ const getModules = (
didcomm: new DidCommModule({
processDidCommMessagesConcurrently: true,

}),
}),
oob: new OutOfBandModule(),
mediationRecipient: new MediationRecipientModule(),
discovery: new DiscoverFeaturesModule(),
Expand All @@ -223,20 +224,43 @@ const getModules = (
serverUrl: fileServerUrl ? fileServerUrl : (process.env.SERVER_URL as string),
}),
openId4VcVerifier: new OpenId4VcVerifierModule({
baseUrl: `http://${process.env.APP_URL}/oid4vp`,

baseUrl: process.env.NODE_ENV === 'PROD' ?
`https://${process.env.APP_URL}/oid4vp`:
`${process.env.AGENT_HTTP_URL}/oid4vp`,
router: openId4VpRouter,
}),
openId4VcIssuer: new OpenId4VcIssuerModule({
baseUrl: `http://${process.env.APP_URL}/oid4vci`,
baseUrl: process.env.NODE_ENV === 'PROD' ?
`https://${process.env.APP_URL}/oid4vci` :
`${process.env.AGENT_HTTP_URL}/oid4vci`,
router: openId4VciRouter,
statefulCredentialOfferExpirationInSeconds: Number(process.env.OPENID_CRED_OFFER_EXPIRY) || 3600,
accessTokenExpiresInSeconds: Number(process.env.OPENID_ACCESS_TOKEN_EXPIRY) || 3600,
authorizationCodeExpiresInSeconds: Number(process.env.OPENID_AUTH_CODE_EXPIRY) || 3600,
cNonceExpiresInSeconds: Number(process.env.OPENID_CNONCE_EXPIRY) || 3600,
dpopRequired: false,
credentialRequestToCredentialMapper: (...args) => getCredentialRequestToCredentialMapper()(...args),
credentialRequestToCredentialMapper: (...args) => getMixedCredentialRequestToCredentialMapper()(...args),
}),
openId4VcHolderModule: new OpenId4VcHolderModule(),
x509: new X509Module({
// getTrustedCertificatesForVerification: (_agentContext, { certificateChain, verification }) => {
// //TODO: We need to trust the certificate tenant wise, for that we need to fetch those details from platform
// const firstCertificate = certificateChain[0]
// console.log(
// `dyncamically trusting certificate ${firstCertificate?.getIssuerNameField('C')?.toString()} for verification of ${
// verification.type
// }`,
// true
// )

// const trustedCertificates = _agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates?.map((cert) =>
// X509Certificate.fromEncodedCertificate(cert).toString('pem')
// ) as [string, ...string[]]

// return [...trustedCertificates]
// }
}),
}
}

Expand Down Expand Up @@ -332,7 +356,7 @@ export async function runRestAgent(restConfig: AriesRestConfig) {
// Ideally for testing connection between tenant agent we need to set this to 'true'. Default is 'false'
// TODO: triage: not sure if we want it to be 'true', as it would mean parallel requests on BW
// Setting it for now //TODO: check if this is needed
allowInsecureHttpUrls: true
allowInsecureHttpUrls: process.env.ALLOW_INSECURE_HTTP_URLS === 'true'
}

async function fetchLedgerData(ledgerConfig: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Agent } from '@credo-ts/core'
import { OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc'
import { Request as Req } from 'express'
import { Body, Controller, Delete, Get, Path, Post, Put, Query, Request, Route, Tags, Security } from 'tsoa'
import { injectable } from 'tsyringe'

import { SCOPES } from '../../../enums'
// eslint-disable-next-line import/order
import ErrorHandlingService from '../../../errorHandlingService'

// import { AgentWithRootOrTenant } from '../../types/agent'
import { OpenId4VcIssuanceSessionsCreateOffer } from '../types/issuer.types'
import { Request as Req } from 'express'

import { issuanceSessionService } from './issuance-sessions.service'
import { SCOPES } from '../../../enums'

/**
* Controller for managing OpenID4VC issuance sessions.
* Provides endpoints to create credential offers, retrieve issuance sessions,
Expand All @@ -25,10 +26,7 @@ export class IssuanceSessionsController extends Controller {
* Creates a credential offer with the specified credential configurations and authorization type.
*/
@Post('/create-credential-offer')
public async createCredentialOffer(
@Request() request: Req,
@Body() options: OpenId4VcIssuanceSessionsCreateOffer,
) {
public async createCredentialOffer(@Request() request: Req, @Body() options: OpenId4VcIssuanceSessionsCreateOffer) {
try {
return await issuanceSessionService.createCredentialOffer(options, request)
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +1,60 @@
import type { RestAgentModules, RestMultiTenantAgentModules } from '../../../cliAgent'
import type { OpenId4VcIssuanceSessionsCreateOffer, X509GenericRecord } from '../types/issuer.types'
import type { Agent } from '@credo-ts/core'
import type { OpenId4VcIssuanceSessionsCreateOffer } from '../types/issuer.types'
import type { OpenId4VcIssuanceSessionState } from '@credo-ts/openid4vc'
import type { Request as Req } from 'express'

import { OpenId4VcIssuanceSessionRepository } from '@credo-ts/openid4vc/build/openid4vc-issuer/repository'

import { SignerMethod } from '../../../enums/enum'
import { BadRequestError, NotFoundError } from '../../../errors/errors'
import { X509_CERTIFICATE_RECORD } from '../../../utils/constant'
import { Request as Req } from 'express'

class IssuanceSessionsService {
public async createCredentialOffer(
options: OpenId4VcIssuanceSessionsCreateOffer,
agentReq: Req
) {
const { credentials, signerOption, publicIssuerId } = options
public async createCredentialOffer(options: OpenId4VcIssuanceSessionsCreateOffer, agentReq: Req) {
const { credentials, publicIssuerId } = options

const issuer = await agentReq.agent.modules.openId4VcIssuer.getIssuerByIssuerId(publicIssuerId)

if (!signerOption || !signerOption.method) {
throw new BadRequestError(`signerOption must be provided with one of: ${Object.values(SignerMethod).join(', ')}`)
}
if (signerOption.method === SignerMethod.Did && !signerOption.did) {
throw new BadRequestError(`'did' must be provided when signer method is 'did'`)
}

const mappedCredentials = credentials.map((c) => {
const supported = issuer.credentialConfigurationsSupported[c.credentialSupportedId]
const mappedCredentials = credentials.map((cred) => {
const supported = issuer.credentialConfigurationsSupported[cred.credentialSupportedId]
if (!supported) {
throw new Error(`CredentialSupportedId '${c.credentialSupportedId}' is not supported by issuer`)
throw new Error(`CredentialSupportedId '${cred.credentialSupportedId}' is not supported by issuer`)
}
if (supported.format !== c.format) {
if (supported.format !== cred.format) {
throw new Error(
`Format mismatch for '${c.credentialSupportedId}': expected '${supported.format}', got '${c.format}'`,
`Format mismatch for '${cred.credentialSupportedId}': expected '${supported.format}', got '${cred.format}'`,
)
}

// must have signing options
if (!cred.signerOptions?.method) {
throw new BadRequestError(
`signerOptions must be provided and allowed methods are ${Object.values(SignerMethod).join(', ')}`,
)
}

if (cred.signerOptions.method == SignerMethod.Did && !cred.signerOptions.did) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : did must be present inside signerOptions if SignerMethod is 'did' `,
)
}

if (cred.signerOptions.method === SignerMethod.X5c && !cred.signerOptions.x5c) {
throw new BadRequestError(
`For ${cred.credentialSupportedId} : x5c must be present inside signerOptions if SignerMethod is 'x5c' `,
)
}

return {
...c,
...cred,
payload: {
...c.payload,
vct: c.payload?.vct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
...cred.payload,
vct: cred.payload?.vct ?? (typeof supported.vct === 'string' ? supported.vct : undefined),
},
}
// format: c.format as OpenId4VciCredentialFormatProfile, TODO: fix this type
})

options.issuanceMetadata ||= {}

if (signerOption.method === SignerMethod.Did) {
options.issuanceMetadata.issuerDid = signerOption.did
} else if (signerOption.method === SignerMethod.X5c) {
const record = (await agentReq.agent.genericRecords.findById(X509_CERTIFICATE_RECORD)) as X509GenericRecord
if (!signerOption.x5c && !record) {
throw new Error('x5c certificate is required')
}
const cert = record?.content?.dcs
const certArray = Array.isArray(cert) ? cert : typeof cert === 'string' ? [cert] : []
if (!certArray.length) {
throw new Error('x509 certificate must be non-empty')
}
options.issuanceMetadata.issuerx509certificate = signerOption.x5c ?? [...certArray]
}

options.issuanceMetadata.credentials = mappedCredentials

const { credentialOffer, issuanceSession } = await agentReq.agent.modules.openId4VcIssuer.createCredentialOffer({
Expand All @@ -76,10 +68,7 @@ class IssuanceSessionsService {
return { credentialOffer, issuanceSession }
}

public async getIssuanceSessionsById(
agentReq: Req,
sessionId: string,
) {
public async getIssuanceSessionsById(agentReq: Req, sessionId: string) {
return agentReq.agent.modules.openId4VcIssuer.getIssuanceSessionById(sessionId)
}

Expand Down Expand Up @@ -115,11 +104,7 @@ class IssuanceSessionsService {
* @param metadata
* @returns the updated issuance session record
*/
public async updateSessionIssuanceMetadataById(
agentReq: Req,
sessionId: string,
metadata: Record<string, unknown>,
) {
public async updateSessionIssuanceMetadataById(agentReq: Req, sessionId: string, metadata: Record<string, unknown>) {
const issuanceSessionRepository = agentReq.agent.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)

const record = await issuanceSessionRepository.findById(agentReq.agent.context, sessionId)
Expand All @@ -144,10 +129,7 @@ class IssuanceSessionsService {
* @param sessionId
* @param issuerAgent
*/
public async deleteById(
agentReq: Req,
sessionId: string,
): Promise<void> {
public async deleteById(agentReq: Req, sessionId: string): Promise<void> {
const issuanceSessionRepository = agentReq.agent.dependencyManager.resolve(OpenId4VcIssuanceSessionRepository)
await issuanceSessionRepository.deleteById(agentReq.agent.context, sessionId)
}
Expand Down
18 changes: 10 additions & 8 deletions src/controllers/openid4vc/types/issuer.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { OpenId4VciCredentialFormatProfile } from "@credo-ts/openid4vc"

export enum SignerMethod {
Did = 'did',
X5c = 'x5c',
}

export interface OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions {
export interface OpenId4VcIssuanceSessionCredentialOptions {
credentialSupportedId: string
format: string
signerOptions: {
method: SignerMethod
did?: string
x5c?: string[]
}
format: OpenId4VciCredentialFormatProfile
payload: {
vct?: string
[key: string]: unknown
Expand All @@ -15,12 +22,7 @@ export interface OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions {

export interface OpenId4VcIssuanceSessionsCreateOffer {
publicIssuerId: string
signerOption: {
method: SignerMethod
did?: string
x5c?: string[]
}
credentials: Array<OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions>
credentials: Array<OpenId4VcIssuanceSessionCredentialOptions>
authorizationCodeFlowConfig?: {
authorizationServerUrl: string
requirePresentationDuringIssuance?: boolean
Expand Down
24 changes: 24 additions & 0 deletions src/controllers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import type {
W3cJsonLdSignCredentialOptions,
W3cCredential,
W3cCredentialSubject,
X509CertificateIssuerAndSubjectOptions,
X509CreateCertificateOptions,
} from '@credo-ts/core'

import type {
Expand Down Expand Up @@ -444,4 +446,26 @@ export type CustomW3cJsonLdSignCredentialOptions = Omit<W3cJsonLdSignCredentialO

export type DisclosureFrame = {
[key: string]: boolean | DisclosureFrame
}


export interface BasicX509CreateCertificateConfig extends X509CertificateIssuerAndSubjectOptions {

keyType: KeyType;
issuerAlternativeNameURL: string;
}

export interface X509ImportCertificateOptionsDto {

/*
X.509 certificate in base64 string format
*/
certificate: string;

/*
Private key in base64 string format
*/
privateKey?: string;

keyType: KeyType;
}
35 changes: 35 additions & 0 deletions src/controllers/x509/crypto-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createPrivateKey, KeyObject } from 'crypto';

/**
* Extracts the raw private key (hex) from a PEM-encoded EC (P-256) private key.
*/
async function pemToRawEcPrivateKey(pem: string): Promise<string> {
const keyObj: KeyObject = createPrivateKey({
key: pem,
format: 'pem',
});

// Extract raw private key (as Buffer)
const rawPrivateKey = keyObj.export({
format: 'jwk',
}).d!;

return Buffer.from(rawPrivateKey, 'base64').toString('hex');
}

/**
* Extracts the raw private key (hex) from a PEM-encoded Ed25519 private key.
*/
export async function pemToRawEd25519PrivateKey(pem: string): Promise<string> {
const keyObj: KeyObject = createPrivateKey({
key: pem.replace(/\\n/g, '\n'),
format: 'pem',
});

// Ed25519 JWK exports the *seed* (first 32 bytes of the private key)
const jwk = keyObj.export({ format: 'jwk' });
if (!jwk.d) throw new Error("Not an Ed25519 private key");

return Buffer.from(jwk.d, 'base64').toString('hex');
}

Loading