Skip to content

Commit

Permalink
improve revocation (#93)
Browse files Browse the repository at this point in the history
* improve revocation

Signed-off-by: Mirko Mollik <[email protected]>

* update credential list and show

Signed-off-by: Mirko Mollik <[email protected]>

* update ci

Signed-off-by: Mirko Mollik <[email protected]>

* add missing dependency

Signed-off-by: Mirko Mollik <[email protected]>

* fix linting

Signed-off-by: Mirko Mollik <[email protected]>

---------

Signed-off-by: Mirko Mollik <[email protected]>
  • Loading branch information
cre8 authored Aug 8, 2024
1 parent 6a9d6bb commit 3e91a9a
Show file tree
Hide file tree
Showing 71 changed files with 729 additions and 361 deletions.
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

0 comments on commit 3e91a9a

Please sign in to comment.