Commit 95240a7d authored by Hidde-Jan Jongsma's avatar Hidde-Jan Jongsma

Implement IRMA Verify

parent 84010bcb
......@@ -30,14 +30,22 @@ export interface ConnectorService {
): Promise<boolean> | boolean;
/**
* Handle fullfillment of a issue request.
* Handle fullfillment of a issue request. This method should perform the
* proper setup for actually issueing credentials to the wallet app for this
* connector.
*
* @returns All data needed for the front-end to set up a issuing session.
*/
handleIssueCredentialRequest(request: CredentialIssueRequest): Promise<any>;
/**
* Handle fullfillment of a verify request.
* Handle fullfillment of a verify request. This method should perform the
* proper setup for actually verifying credentials to the wallet app for this
* connector.
*
* @returns All data needed for the front-end to set up a verifying session.
*/
handleVerifyCredentialRequest(request: CredentialIssueRequest): Promise<any>;
handleVerifyCredentialRequest(request: CredentialVerifyRequest): Promise<any>;
// registerRoutes(root: string, app: Express): void;
......
......@@ -8,6 +8,6 @@ import { GetConnectorPipe } from './get-connector.pipe';
@Module({
imports: [JolocomModule, IrmaModule],
providers: [ConnectorsService, GetConnectorPipe],
exports: [ConnectorsService, GetConnectorPipe],
exports: [ConnectorsService, GetConnectorPipe, JolocomModule, IrmaModule],
})
export class ConnectorsModule {}
This diff is collapsed.
import { Injectable } from '@nestjs/common';
import { Injectable, NotImplementedException } from '@nestjs/common';
import { ConnectorService } from '../connector-service.interface';
import { Organization } from 'src/organizations/organization.entity';
import { CredentialIssueRequest } from 'src/requests/credential-issue-request.entity';
import { CredentialVerifyRequest } from 'src/requests/credential-verify-request.entity';
import { sign, verify } from 'jsonwebtoken';
import * as irmaCredentials from './irma-credentials.json';
type IrmaDisclosureConjunction = string[][][];
type IrmaAttribute = {
rawvalue: string;
value: {
'': any;
en?: any;
nl?: any;
};
id: string;
status: string;
issueancetime: number;
};
type IrmaDisclosure = IrmaAttribute[][];
interface IrmaSessionResult {
status: string;
proofStatus: string;
disclosed: IrmaDisclosure;
}
interface IrmaDisclosureRequest {
'@context': 'https://irma.app/ld/request/disclosure/v2';
disclose: IrmaDisclosureConjunction;
}
// TODO Move to config / env
const JWT_KEY = `-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----`;
const IRMASERVER_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0AmPwCCs3ZCwI4ehYiTQ
JQkh3JJSh1BrbvRfq5ib2F9KqYRI+U/39KrQvpdLBHF7Kl/ZN/SREykn17PyzkXV
Thy0DaRci5l9K2LFQf0DYg+Tmt/+tf8EHFurD00uegTSDuLxDBDbwuVHnQfgBZol
WlM35+leRK4szrk5nd65ZzDbpy12sSpbP2EfhawoYpa/8ziR2SKcPn7ezBItJdFk
SWP+6fbWdrvoKgk1bR3vX4+nITs4K6A4r8pZwKLVl9TRH9sdzeyuZhCerfgNHQAq
dRYFl6Hs5prAbnDNOXWzCeqKdBI55TqVJ7S7VMlX5OZCDvpSjbq3I0p+Cpm/FSqW
GyZarTT8o1K2za/4FKJo7GwHuOJDp09BCPPHEiz5vzfypArPn725S/PnZPTCc3Be
r1gV6xT18rUd/Uu0A6JU0aTEHGEcmpiXraldN26Av+htS5CXdPFMejeQZojZITmV
Wd2eSX5VcGQTXybV/RhwBRGKdaJOctMWjEtFg4DCRHGFlKWmZjNLlEwMWEJNKI2J
bghRQplLqKtX7vUJvp8S1d9NC6gNFnlBNG393GuN8N/JRpxWq5metK5WqrGgpkET
7e/cwsR2JqcBdpdH/6SVbDAvIw2FoPz580V1JeqjRRNbh0XsHWME+NTNyQWLSG9d
CAkT1FVbDPaMs4US5ixQe7ECAwEAAQ==
-----END PUBLIC KEY-----`;
interface IrmaCredential {
id: string;
}
@Injectable()
export class IrmaService implements ConnectorService {
......@@ -26,11 +128,103 @@ export class IrmaService implements ConnectorService {
return !!request.type.irmaType;
}
async handleIssueCredentialRequest(request: CredentialIssueRequest) {
return null;
async handleIssueCredentialRequest(issueRequest: CredentialIssueRequest) {
throw new NotImplementedException('Cannot issue IRMA credentials');
}
async handleVerifyCredentialRequest(verifyRequest: CredentialVerifyRequest) {
return {
jwt: this.getSignedDisclosureJwt(verifyRequest),
server: process.env.IRMASERVER_URL,
};
}
validateIrmaDisclosure(verifyRequest: CredentialVerifyRequest, jwt: string) {
const publicKey = IRMASERVER_PUBLIC_KEY;
console.log(verifyRequest, jwt, publicKey);
const decoded = verify(jwt, publicKey, {
issuer: 'irmaserver',
subject: 'disclosing_result',
});
let result: IrmaSessionResult;
if (typeof decoded !== 'object') {
throw new Error('Could not properly decode JWT');
} else {
result = decoded as IrmaSessionResult;
}
if (result.status !== 'DONE') {
throw new Error('Invalid session status');
}
if (result.proofStatus !== 'VALID') {
throw new Error('Invalid proof status');
}
// TODO: Validate disclosed info satisfies credentialType type.
return this.transformDisclosure(result.disclosed);
}
protected transformDisclosure(disclose: IrmaDisclosure) {
const data = {};
disclose.forEach(arr =>
arr.forEach(attribute => {
const keyParts = attribute.id.split('.');
const key = keyParts[keyParts.length - 1];
data[key] = attribute.rawvalue;
}),
);
return data;
}
async handleVerifyCredentialRequest(request: CredentialIssueRequest) {
return null;
protected getSignedDisclosureJwt(
verifyRequest: CredentialVerifyRequest,
): string {
const disclosureRequest = this.getIrmaDisclosureRequest(verifyRequest);
return this.signDisclosureSessionJwt(disclosureRequest);
}
protected getIrmaDisclosureRequest(
verifyRequest: CredentialVerifyRequest,
): IrmaDisclosureRequest {
const irmaCredentialId = verifyRequest.type.irmaType;
const irmaCredential = irmaCredentials.find(c => c.id === irmaCredentialId);
if (!irmaCredential) {
throw new Error(
`Could not find irma credential with id ${verifyRequest.type.irmaType}`,
);
}
const attributes = irmaCredential.attributes as IrmaCredential[];
return {
'@context': 'https://irma.app/ld/request/disclosure/v2',
disclose: [[attributes.map(({ id }) => `${irmaCredentialId}.${id}`)]],
};
}
protected signDisclosureSessionJwt(
disclosureRequest: IrmaDisclosureRequest,
): string {
// TODO: Get from config
const issuer = 'ssi-service-provider';
const subject = 'verification_request';
const key = JWT_KEY;
return sign(
{ sprequest: { validity: 120, request: disclosureRequest } },
key,
{
algorithm: 'RS256',
issuer,
subject,
},
);
}
}
......@@ -56,7 +56,7 @@ export class JolocomService implements ConnectorService {
return null;
}
async handleVerifyCredentialRequest(request: CredentialIssueRequest) {
async handleVerifyCredentialRequest(request: CredentialVerifyRequest) {
return null;
}
......
export enum ResponseStatus {
success = 'success',
error = 'error',
cancelled = 'cancelled',
}
import { Controller, Get, Query, Param } from '@nestjs/common';
import { Controller, Get, Query, Param, Post } from '@nestjs/common';
import {
DecodeIssueRequestPipe,
......@@ -27,11 +27,11 @@ export class IssueController {
}
@Get(':connector')
async handleCredentialVerifyRequest(
async handleCredentialIssueRequest(
@Param('connector', GetConnectorPipe) connectorService: ConnectorService,
@Query('issueRequestId', GetIssueRequestPipe)
issueRequest: CredentialIssueRequest,
) {
return { issueRequest, connectorService };
return connectorService.handleIssueCredentialRequest(issueRequest);
}
}
......@@ -31,6 +31,9 @@ export class CredentialIssueRequest implements CredentialRequest {
@ManyToOne(
() => CredentialType,
type => type.issueRequests,
{
eager: true,
},
)
type: CredentialType;
......@@ -44,10 +47,13 @@ export class CredentialIssueRequest implements CredentialRequest {
@ManyToOne(
() => Organization,
organization => organization.issueRequests,
{
eager: true,
},
)
requestor: Organization;
static requestType: 'credential-issue-request';
static requestType: string;
get requestId() {
return `${CredentialIssueRequest.requestType}:${this.uuid}`;
......@@ -65,3 +71,7 @@ export class CredentialIssueRequest implements CredentialRequest {
this.requestor = issuer;
}
}
// This was moved outside the class definition, because TypeScript didn't emit
// the property into JS.
CredentialIssueRequest.requestType = 'credential-issue-request';
......@@ -26,6 +26,9 @@ export class CredentialVerifyRequest implements CredentialRequest {
@ManyToOne(
() => CredentialType,
type => type.verifyRequests,
{
eager: true,
},
)
type: CredentialType;
......@@ -36,10 +39,13 @@ export class CredentialVerifyRequest implements CredentialRequest {
@ManyToOne(
() => Organization,
organization => organization.verifyRequests,
{
eager: true,
},
)
requestor: Organization;
static requestType: 'credential-verify-request';
static requestType: string;
get requestId() {
return `${CredentialVerifyRequest.requestType}:${this.uuid}`;
......@@ -57,3 +63,7 @@ export class CredentialVerifyRequest implements CredentialRequest {
this.requestor = verifier;
}
}
// This was moved outside the class definition, because TypeScript didn't emit
// the property into JS.
CredentialVerifyRequest.requestType = 'credential-verify-request';
......@@ -2,12 +2,20 @@ import {
SubscribeMessage,
WebSocketGateway,
MessageBody,
ConnectedSocket,
WebSocketServer,
} from '@nestjs/websockets';
import { Socket, Server } from 'socket.io';
import { CredentialVerifyRequest } from './credential-verify-request.entity';
import { DecodeVerifyRequestPipe } from './get-request.pipe';
import { DecodeVerifyRequestPipe } from './requests.pipe';
import { ResponseStatus } from 'src/connectors/response-status.enum';
@WebSocketGateway()
export class RequestsGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage('message')
handleMessage(@MessageBody() message: string): string {
return `${message.toLocaleUpperCase()}!`;
......@@ -20,4 +28,24 @@ export class RequestsGateway {
): CredentialVerifyRequest {
return verifyRequest;
}
@SubscribeMessage('request-started')
handleRequestStarted(
@MessageBody()
requestId: string,
@ConnectedSocket()
client: Socket,
) {
console.log('Client', client.id, 'joined', requestId);
client.join(requestId);
}
sendRedirectResponse(
requestId: string,
status: ResponseStatus,
redirectUrl: string,
) {
console.log('In gateway, sending', requestId, status);
this.server.to(requestId).emit('redirect', { status, redirectUrl });
}
}
import { Injectable, Logger } from '@nestjs/common';
import { decode, verify } from 'jsonwebtoken';
import { decode, verify, sign, SignOptions } from 'jsonwebtoken';
import { OrganizationsService } from 'src/organizations/organizations.service';
import { Organization } from 'src/organizations/organization.entity';
import {
CredentialVerifyRequest,
......@@ -14,6 +13,7 @@ import {
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { CredentialType } from 'src/types/credential-type.entity';
import { ResponseStatus } from 'src/connectors/response-status.enum';
export class InvalidRequestJWT extends Error {}
......@@ -47,7 +47,10 @@ export class RequestsService {
async findVerifyRequestByRequestId(requestId: string) {
const [type, uuid] = requestId.split(':');
console.log(type, CredentialVerifyRequest.requestType, uuid);
if (type !== CredentialVerifyRequest.requestType || !uuid) {
console.log(type !== CredentialVerifyRequest.requestType, !uuid);
return null;
}
......@@ -83,6 +86,7 @@ export class RequestsService {
}
async decodeVerifyRequestToken(jwt: string) {
// TODO: verify sub(ject)
const { request, requestor } = await this.decodeAndVerifyJwt<
CredentialVerifyRequestData
>(jwt);
......@@ -97,6 +101,8 @@ export class RequestsService {
},
);
// TODO: load from db if request already exists.
const verifyRequest = new CredentialVerifyRequest();
verifyRequest.requestor = requestor;
......@@ -107,6 +113,8 @@ export class RequestsService {
}
async decodeIssueRequestToken(jwt: string) {
// TODO: verify sub(ject)
const { request, requestor } = await this.decodeAndVerifyJwt<
CredentialIssueRequestData
>(jwt);
......@@ -121,6 +129,8 @@ export class RequestsService {
},
);
// TODO: load from db if request already exists.
const issueRequest = new CredentialIssueRequest();
issueRequest.requestor = requestor;
......@@ -168,4 +178,56 @@ export class RequestsService {
throw new InvalidRequestJWT('Could not decode request JWT');
}
}
encodeVerifyRequestResponse(
verifyRequest: CredentialVerifyRequest,
status: ResponseStatus,
connectorName: string,
data: any,
) {
return this.encodeResponse(
{
requestId: verifyRequest.uuid, // TODO: use jwtid from request
type: verifyRequest.type.type,
status,
connector: connectorName,
data,
},
verifyRequest.verifier,
{
subject: 'credential-verify-response',
},
);
}
encodeIssueRequestResponse(
issueRequest: CredentialIssueRequest,
status: ResponseStatus,
connectorName: string,
) {
return this.encodeResponse(
{
requestId: issueRequest.uuid, // TODO: use jwtid from request
type: issueRequest.type.type,
status,
connector: connectorName,
},
issueRequest.issuer,
{
subject: 'credential-issue-response',
},
);
}
encodeResponse(
payload: any,
organization: Organization,
options: SignOptions = {},
) {
return sign(payload, organization.sharedSecret, {
issuer: 'ssi-service-provider', // TODO Get from config?
audience: organization.uuid,
...options,
});
}
}
......@@ -29,6 +29,7 @@ export class CredentialType {
credentialType => credentialType.credentialTypes,
{
nullable: true,
eager: true,
},
)
jolocomType: JolocomCredentialType;
......
import { Injectable } from '@nestjs/common';
import { sign } from 'jsonwebtoken';
import { sign, SignOptions } from 'jsonwebtoken';
import { randomBytes } from 'crypto';
......@@ -9,12 +9,17 @@ const JWT_ID_SIZE = 9;
@Injectable()
export class UtilsService {
createSignedJwt(data: string | object, organization: Organization) {
createSignedJwt(
data: string | object,
organization: Organization,
options: SignOptions = {},
) {
const jwtId = randomBytes(JWT_ID_SIZE).toString('base64');
return sign(data, organization.sharedSecret, {
jwtid: jwtId,
issuer: organization.uuid,
...options,
});
}
}
import { Controller, Get, Param, Query } from '@nestjs/common';
import { Controller, Get, Param, Query, Post, Body } from '@nestjs/common';
import { GetConnectorPipe } from '../connectors/get-connector.pipe';
import { ConnectorService } from '../connectors/connector-service.interface';
......@@ -10,12 +10,17 @@ import {
} from '../requests/requests.pipe';
import { CredentialVerifyRequest } from '../requests/credential-verify-request.entity';
import { RequestsGateway } from '../requests/requests.gateway';
import { IrmaService } from 'src/connectors/irma/irma.service';
import { RequestsService } from 'src/requests/requests.service';
import { ResponseStatus } from 'src/connectors/response-status.enum';
@Controller('api/verify')
export class VerifyController {
constructor(
private gateway: RequestsGateway,
private connectorsService: ConnectorsService,
private irmaService: IrmaService,
private requestsService: RequestsService,
) {
console.log(this.gateway);
}
......@@ -39,6 +44,40 @@ export class VerifyController {
@Query('verifyRequestId', GetVerifyRequestPipe)
verifyRequest: CredentialVerifyRequest,
) {
return { verifyRequest, connectorService };
console.log(verifyRequest, connectorService);
return connectorService.handleVerifyCredentialRequest(verifyRequest);
}
@Post('irma/disclose')
handleIrmaVerifyDisclosure(
@Query('verifyRequestId', GetVerifyRequestPipe)
verifyRequest: CredentialVerifyRequest,
@Body('jwt')
irmaJwt: string,
) {
try {
const result = this.irmaService.validateIrmaDisclosure(
verifyRequest,
irmaJwt,
);
const responseToken = this.requestsService.encodeVerifyRequestResponse(
verifyRequest,
ResponseStatus.success,
'irma',
result,
);
this.gateway.sendRedirectResponse(
verifyRequest.requestId,
ResponseStatus.success,
new URL(
`?response=${responseToken}`,
verifyRequest.callbackUrl,
).toString(),
);
} catch {
// TODO: handle bad flow
}
}
}
......@@ -5,6 +5,7 @@
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment