Skip to content

Commit

Permalink
improve revocation
Browse files Browse the repository at this point in the history
Signed-off-by: Mirko Mollik <mirkomollik@gmail.com>
  • Loading branch information
cre8 committed Aug 7, 2024
1 parent 6a9d6bb commit 2ae7caf
Show file tree
Hide file tree
Showing 58 changed files with 520 additions and 176 deletions.
1 change: 0 additions & 1 deletion apps/holder-app-e2e/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"projectType": "application",
"sourceRoot": "apps/holder-app-e2e/src",
"implicitDependencies": ["holder-app"],
"// targets": "to see all targets run: nx show project holder-app-e2e --web",
"targets": {
"dev": {
"command": "cd apps/holder-app-e2e && playwright test --ui"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { CredentialsController } from './credentials.controller';
import { CredentialsService } from './credentials.service';
import { Credential } from './entities/credential.entity';
import { HttpModule } from '@nestjs/axios';
import { CryptoModule, ResolverModule } from '@credhub/backend';

@Module({
imports: [TypeOrmModule.forFeature([Credential])],
imports: [
TypeOrmModule.forFeature([Credential]),
HttpModule,
ResolverModule,
CryptoModule,
],
controllers: [CredentialsController],
providers: [CredentialsService],
exports: [CredentialsService],
Expand Down
60 changes: 54 additions & 6 deletions apps/holder-backend/src/app/credentials/credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import { OnEvent } from '@nestjs/event-emitter';
import { USER_DELETED_EVENT, UserDeletedEvent } from '../auth/auth.service';
import { Interval } from '@nestjs/schedule';
import { createHash } from 'crypto';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { Verifier } from '@sd-jwt/types';
import { JWK, JWTPayload } from '@sphereon/oid4vci-common';
import { CryptoService, ResolverService } from '@credhub/backend';
import { getListFromStatusListJWT } from '@sd-jwt/jwt-status-list';

type DateKey = 'exp' | 'nbf';
@Injectable()
Expand All @@ -25,18 +31,57 @@ export class CredentialsService {

constructor(
@InjectRepository(Credential)
private credentialRepository: Repository<Credential>
private credentialRepository: Repository<Credential>,
private httpService: HttpService,
private resolverService: ResolverService,
private cryptoService: CryptoService
) {
const verifier: Verifier = async (data, signature) => {
const decodedVC = await this.instance.decode(`${data}.${signature}`);
const payload = decodedVC.jwt?.payload as JWTPayload;
const header = decodedVC.jwt?.header as JWK;
const publicKey = await this.resolverService.resolvePublicKey(
payload,
header
);
//get the verifier based on the algorithm
const crypto = this.cryptoService.getCrypto(header.alg);
const verify = await crypto.getVerifier(publicKey);
return verify(data, signature).catch((err) => {
console.log(err);
return false;
});
};

/**
* Fetch the status list from the uri.
* @param uri
* @returns
*/
const statusListFetcher: (uri: string) => Promise<string> = async (
uri: string
) => {
const response = await firstValueFrom(this.httpService.get(uri));
return response.data;
};

this.instance = new SDJwtVcInstance({
hasher: digest,
verifier: () => Promise.resolve(true),
statusListFetcher,
statusValidator(status) {
if (status === 1) {
throw new Error('Status is not valid');
}
return Promise.resolve();
},
verifier,
});
}

/**
* Start an interval to update the status of the credentials. This is relevant for showing active credentials.
*/
@Interval(1000 * 10)
@Interval(1000 * 3)
updateStatusInterval() {
this.updateStatus();
}
Expand Down Expand Up @@ -134,11 +179,13 @@ export class CredentialsService {
* Updates the state of a credential. This is relevant for showing active credentials.
*/
async updateStatus() {
//TODO: should we also set the state on expired?

//we are going for all credentials where credentials are not expired. It could happen that the status of a revoked credential will change.
const credentials = await this.credentialRepository.find({
where: { exp: MoreThanOrEqual(Date.now()) },
where: {
//exp: MoreThanOrEqual(Date.now() / 1000),
//only a valid credential has an empty status.
status: IsNull(),
},
});
for (const credential of credentials) {
await this.instance.verify(credential.value).then(
Expand All @@ -150,6 +197,7 @@ export class CredentialsService {
}
},
async (err: Error) => {
console.log(err);
if (err.message.includes('Status is not valid')) {
//update the status in the db.
credential.status = CredentialStatus.REVOKED;
Expand Down
26 changes: 11 additions & 15 deletions apps/holder-backend/src/app/oid4vc/oid4vp/oid4vp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export class Oid4vpService {

//parse the uri
const parsedAuthReqURI = await op.parseAuthorizationRequestURI(data.url);
const verifiedAuthReqWithJWT: VerifiedAuthorizationRequest =
await op.verifyAuthorizationRequest(
parsedAuthReqURI.requestObjectJwt as string,
{}
);
const verifiedAuthReqWithJWT: VerifiedAuthorizationRequest = await op
.verifyAuthorizationRequest(parsedAuthReqURI.requestObjectJwt, {})
.catch(() => {
throw new ConflictException('Invalid request');
});
const issuer =
(
verifiedAuthReqWithJWT.authorizationRequestPayload
Expand All @@ -71,6 +71,10 @@ export class Oid4vpService {
const credentials = (await this.credentialsService.findAll(user)).map(
(entry) => entry.value
);

if (credentials.length === 0) {
throw new ConflictException('No matching credentials found');
}
//init the pex instance
const pex = new PresentationExchange({
allVerifiableCredentials: credentials,
Expand All @@ -82,16 +86,12 @@ export class Oid4vpService {
await PresentationExchange.findValidPresentationDefinitions(
verifiedAuthReqWithJWT.authorizationRequestPayload
);
// throws in error in case none was provided
if (pds.length === 0) {
throw new Error('No matching credentials found');
}

await this.sessionRepository.save(
this.sessionRepository.create({
id: sessionId,
// we need to store the JWT, because it serializes an object that can not be stored in the DB
requestObjectJwt: parsedAuthReqURI.requestObjectJwt as string,
requestObjectJwt: parsedAuthReqURI.requestObjectJwt,
user,
pds,
})
Expand All @@ -100,11 +100,7 @@ export class Oid4vpService {
// select the credentials for the presentation
const result = await pex
.selectVerifiableCredentialsForSubmission(pds[0].definition)
.catch((err: SelectResults) => {
console.log(err);
if (err.errors.length > 0) {
throw new ConflictException(err.errors);
}
.catch(() => {
//instead of throwing an error, we return an empty array. This allows the user to show who sent the request for what.
return { verifiableCredential: [] };
});
Expand Down
1 change: 0 additions & 1 deletion apps/issuer-backend-e2e/src/support/global-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@
module.exports = async function () {
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
// Hint: `globalThis` is shared between setup and teardown.
console.log(globalThis.__TEARDOWN_MESSAGE__);
};
5 changes: 3 additions & 2 deletions apps/issuer-backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
import {
AuthModule,
CRYPTO_VALIDATION_SCHEMA,
KEY_VALIDATION_SCHEMA,
KeyModule,
OIDC_VALIDATION_SCHEMA,
DB_VALIDATION_SCHEMA,
DbModule,
} from '@credhub/relying-party-shared';
import { DB_VALIDATION_SCHEMA, DbModule } from '@credhub/relying-party-shared';
import { CredentialsModule } from './credentials/credentials.module';
import { StatusModule } from './status/status.module';
import { ScheduleModule } from '@nestjs/schedule';
import { IssuerModule } from './issuer/issuer.module';
import { CRYPTO_VALIDATION_SCHEMA } from '@credhub/backend';

@Module({
imports: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import { AuthGuard } from 'nest-keycloak-connect';
export class CredentialsController {
constructor(private readonly credentialsService: CredentialsService) {}

@Get()
findAll() {
return this.credentialsService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.credentialsService.findOne(id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export class CredentialsService {
}

findOne(id: string) {
return this.credentialRepository.findOneOrFail({ where: { id } });
return this.credentialRepository.findOneByOrFail({ id });
}

getBySessionId(sessionId: string) {
return this.credentialRepository.findBy({ sessionId });
}

remove(id: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ export class Credential {

@Column()
value: string;

@Column()
sessionId: string;
}
8 changes: 8 additions & 0 deletions apps/issuer-backend/src/app/issuer/dto/session-entry.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Credential } from '../../credentials/entities/credential.entity';
import { CredentialOfferSessionEntity } from '../entities/credential-offer-session.entity';

export class SessionEntryDto {
session: CredentialOfferSessionEntity;

credentials: Credential[];
}
15 changes: 12 additions & 3 deletions apps/issuer-backend/src/app/issuer/issuer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ import { AuthGuard } from 'nest-keycloak-connect';
import { SessionResponseDto } from './dto/session-response.dto';
import { CredentialOfferSession } from './dto/credential-offer-session.dto';
import { DBStates } from '@credhub/relying-party-shared';
import { CredentialsService } from '../credentials/credentials.service';
import { SessionEntryDto } from './dto/session-entry.dto';

@UseGuards(AuthGuard)
@ApiOAuth2([])
@ApiTags('sessions')
@Controller('sessions')
export class IssuerController {
constructor(private issuerService: IssuerService) {}
constructor(
private issuerService: IssuerService,
private credentialsService: CredentialsService
) {}

@ApiOperation({ summary: 'Lists all sessions' })
@Get()
Expand All @@ -34,15 +39,19 @@ export class IssuerController {

@ApiOperation({ summary: 'Returns the status for a session' })
@Get(':id')
async getSession(@Param('id') id: string): Promise<CredentialOfferSession> {
async getSession(@Param('id') id: string): Promise<SessionEntryDto> {
const session =
(await this.issuerService.vcIssuer.credentialOfferSessions.get(
id
)) as CredentialOfferSession;
if (!session) {
throw new NotFoundException(`Session with id ${id} not found`);
}
return session;
const credentials = await this.credentialsService.getBySessionId(id);
return {
session,
credentials: credentials,
};
}

@ApiOperation({ summary: 'Creates a new session request' })
Expand Down
22 changes: 13 additions & 9 deletions apps/issuer-backend/src/app/issuer/issuer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ import {
import { IssuerDataService } from './issuer-data.service';
import { SessionRequestDto } from './dto/session-request.dto';
import { CredentialsService } from '../credentials/credentials.service';
import {
CryptoImplementation,
CryptoService,
DBStates,
KeyService,
} from '@credhub/relying-party-shared';
import { DBStates, KeyService } from '@credhub/relying-party-shared';
import { IssuerMetadata } from './types';
import { StatusService } from '../status/status.service';
import { SessionResponseDto } from './dto/session-response.dto';
Expand All @@ -45,7 +40,7 @@ import { Repository } from 'typeorm';
import { CNonceEntity } from './entities/c-nonce.entity';
import { URIStateEntity } from './entities/uri-state.entity';
import { CredentialOfferSessionEntity } from './entities/credential-offer-session.entity';

import { CryptoImplementation, CryptoService } from '@credhub/backend';
interface CredentialDataSupplierInput {
credentialSubject: Record<string, unknown>;
exp: number;
Expand All @@ -56,6 +51,8 @@ export class IssuerService {
private express: ExpressSupport;
vcIssuer: VcIssuer<DIDDocument>;

sessionMapper: Map<string, string> = new Map();

private crypto: CryptoImplementation;

constructor(
Expand Down Expand Up @@ -185,16 +182,21 @@ export class IssuerService {
};

const credential: SdJwtDecodedVerifiableCredentialPayload = {
iat: new Date().getTime(),
iat: Math.round(new Date().getTime() / 1000),
iss: args.credentialOffer.credential_offer.credential_issuer,
vct: (args.credentialRequest as CredentialRequestSdJwtVc).vct,
jti: v4(),
...(args.credentialDataSupplierInput as CredentialDataSupplierInput)
.credentialSubject,
//TODO: can be removed when correct type is set in PEX
status: status as unknown as { idx: number; uri: string },
//TODO: validate that the seconds and not milliseconds are used
exp: args.credentialDataSupplierInput.exp,
nbf: args.credentialDataSupplierInput.nbf,
};

// map the credential id with the session because we will be not able to get the session id in the sign callback. We are using the pre auth code for now.
this.sessionMapper.set(credential.jti as string, args.preAuthorizedCode);
return Promise.resolve({
credential,
format: 'vc+sd-jwt',
Expand Down Expand Up @@ -253,12 +255,14 @@ export class IssuerService {
),
{ header: { kid: await this.keyService.getKid() } }
);
const sessionId = this.sessionMapper.get(args.credential.jti as string);
this.sessionMapper.delete(args.credential.jti as string);
await this.credentialsService.create({
value: jwt,
id: args.credential.jti as string,
sessionId,
});
return jwt;
// return decodeSdJwtVc(jwt, digest) as unknown as Promise<CompactSdJwtVc>;
};

//create the issuer instance
Expand Down
2 changes: 1 addition & 1 deletion apps/issuer-backend/src/app/status/status.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class StatusController {
@Public()
@ApiOperation({ summary: 'Get the status of a specific index' })
@Get(':id/:index')
getStatus(@Param('id') id: string, @Param('index') index: string) {
getStatus(@Param('id') id: string, @Param('index') index: number) {
return this.statusService.getStatus(id, index).then((status) => ({
status,
}));
Expand Down
5 changes: 3 additions & 2 deletions apps/issuer-backend/src/app/status/status.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,14 @@ export class StatusService {
* @param index
* @returns
*/
getStatus(id: string, index: string) {
getStatus(id: string, index: number) {
return this.statusRepository.findOneBy({ id }).then((list) => {
if (!list) {
throw new NotFoundException();
}
const decodedList = this.decodeList(list.list);
const statusList = new List(decodedList, list.bitsPerStatus);
return statusList.getStatus(parseInt(index));
return statusList.getStatus(index);
});
}

Expand All @@ -196,6 +196,7 @@ export class StatusService {
listEntry.jwt = jwt;
listEntry.exp = exp;
await this.statusRepository.save(listEntry);
console.log('Status set');
}

/**
Expand Down
Loading

0 comments on commit 2ae7caf

Please sign in to comment.