Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

improve revocation #93

Merged
merged 5 commits into from
Aug 8, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:

# build the docker image for keycloak
- name: Build Keycloak Docker Image
run: cd deploys/keycloak && docker-compose build keycloak && docker-compose push keycloak
run: cd deploys/keycloak && docker compose build keycloak && docker compose push keycloak

# add the release, build the container and release it with the information for sentry
- name: Build and push images
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: npx playwright install --with-deps

- name: Build keycloak
run: cd deploys/keycloak && docker-compose build keycloak
run: cd deploys/keycloak && docker compose build keycloak

- name: Add entry to /etc/hosts
run: echo "127.0.0.1 host.testcontainers.internal" | sudo tee -a /etc/hosts
Expand All @@ -56,17 +56,26 @@ jobs:
# token: ${{ secrets.CODECOV_TOKEN }}

- name: Upload playwright results
if: always()
if: always() && steps.check_playwright_results.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: dist/.playwright
retention-days: 30

- name: Check if playwright results exist
id: check_playwright_results
run: echo "exists=$(if [ -d dist/.playwright ]; then echo true; else echo false; fi)" >> $GITHUB_ENV


- name: Upload testcontainer logs
if: always()
if: always() && steps.check_testcontainer_logs.outputs.exists == 'true'
uses: actions/upload-artifact@v4
with:
name: testcontainer-logs
path: tmp/logs
retention-days: 30

- name: Check if testcontainer logs exist
id: check_testcontainer_logs
run: echo "exists=$(if [ -d tmp/logs ]; then echo true; else echo false; fi)" >> $GITHUB_ENV
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
10 changes: 6 additions & 4 deletions apps/holder-app/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@ export const routes: Routes = [
{
path: 'credentials',
component: CredentialsListComponent,
},
{
path: 'credentials/:id',
component: CredentialsShowComponent,
children: [
{
path: ':id',
component: CredentialsShowComponent,
},
],
},
{
path: 'history',
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
Loading