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
89 changes: 80 additions & 9 deletions src/did/controllers/did.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Controller,
Post,
Get,
Patch,
Body,
Param,
HttpException,
Expand Down Expand Up @@ -29,6 +30,38 @@ export class DidController {
private readonly vcRepo: Repository<VerifiableCredential>,
) {}

// ── DID Document Management ────────────────────────────────────────────────

@Post('register')
@ApiOperation({ summary: 'Register a new DID and anchor its DID document' })
@ApiResponse({ status: 201, description: 'DID document created' })
async registerDid(
@Body('did') did: string,
@Body('publicKey') publicKey: string,
) {
if (!did || !publicKey) {
throw new HttpException(
'did and publicKey are required',
HttpStatus.BAD_REQUEST,
);
}
const document = await this.didAuthService.registerDid(did, publicKey);
return document;
}

@Get('document/:did')
@ApiOperation({ summary: 'Resolve a W3C-compliant DID document' })
@ApiResponse({ status: 200, description: 'W3C DID document' })
async resolveDidDocument(@Param('did') did: string) {
const document = await this.didAuthService.resolveDidDocument(did);
if (!document) {
throw new HttpException('DID not found', HttpStatus.NOT_FOUND);
}
return document;
}

// ── DID Authentication ─────────────────────────────────────────────────────

@Post('auth/challenge')
@ApiOperation({
summary: 'Generate a cryptographic nonce challenge for DID authentication',
Expand Down Expand Up @@ -58,18 +91,48 @@ export class DidController {
did,
signature,
);
return sessionToken; // Returns standard app token session
return sessionToken;
}

// ── Verifiable Credentials ─────────────────────────────────────────────────

@Post('credentials/issue')
@ApiOperation({ summary: 'Issue a KYC verifiable credential for a DID' })
@ApiResponse({ status: 201, description: 'VC issued successfully' })
async issueCredential(
@Body('userDid') userDid: string,
@Body('kycTier') kycTier: number,
@Body('fullName') fullName: string,
@Body('dateOfBirth') dateOfBirth: string,
) {
if (!userDid || kycTier === undefined || !fullName || !dateOfBirth) {
throw new HttpException(
'userDid, kycTier, fullName, and dateOfBirth are required',
HttpStatus.BAD_REQUEST,
);
}
const vc = await this.vcIssuerService.issueKycCredential(
userDid,
kycTier,
fullName,
dateOfBirth,
);
return {
id: vc.id,
did: vc.did,
issuerDid: vc.issuerDid,
credentialType: vc.credentialType,
status: vc.status,
issuedAt: vc.issuedAt,
};
}

@Get('credentials/:did')
@ApiOperation({
summary:
'Retrieve public privacy-preserving schemas for a particular DID profile',
summary: 'Retrieve public credential metadata for a DID (no PII exposed)',
})
async getCredentials(@Param('did') did: string) {
// Only return non-PII attributes off the VCs (hiding the encrypted payloads)
const vcs = await this.vcRepo.find({ where: { did } });

return vcs.map((vc) => ({
id: vc.id,
credentialType: vc.credentialType,
Expand All @@ -79,15 +142,23 @@ export class DidController {
}));
}

@Patch('credentials/:id/revoke')
@ApiOperation({ summary: 'Revoke a verifiable credential by ID' })
@ApiResponse({ status: 200, description: 'Credential revoked' })
async revokeCredential(@Param('id') id: string) {
const revoked = await this.vcIssuerService.revokeCredential(id);
return { revoked, vcId: id, revokedAt: new Date().toISOString() };
}

// ── Zero-Knowledge Proofs ──────────────────────────────────────────────────

@Post('verify-proof')
@ApiOperation({
summary:
'Third-party API for ZKP credential verification (KYC/AML Compliance Rules)',
summary: 'Verify a ZKP credential proof without revealing raw KYC data',
})
@ApiResponse({
status: 200,
description:
'Returns boolean validating the zero-knowledge proof constraint',
description: 'Returns boolean validating the zero-knowledge proof',
})
async verifyZkp(@Body() payload: ZkProofPayload) {
if (!payload.userDid || !payload.vcId || !payload.zkpProofSignature) {
Expand Down
71 changes: 64 additions & 7 deletions src/did/services/did-auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import {
Injectable,
UnauthorizedException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DidDocument } from '../entities/did-document.entity';
Expand All @@ -12,20 +16,52 @@ export class DidAuthService {
private readonly didDocRepo: Repository<DidDocument>,
) {}

/**
* Register a new DID and anchor its W3C DID document.
* Returns a minimal W3C DID Core compliant document.
*/
async registerDid(
did: string,
publicKey: string,
): Promise<Record<string, unknown>> {
const existing = await this.didDocRepo.findOne({ where: { did } });
if (existing) {
throw new ConflictException(`DID '${did}' is already registered`);
}

const doc = this.didDocRepo.create({
did,
publicKey,
userId: crypto.randomUUID(),
});
await this.didDocRepo.save(doc);

return this.buildW3cDocument(doc);
}

/**
* Resolve a DID to its W3C-compliant DID document.
*/
async resolveDidDocument(
did: string,
): Promise<Record<string, unknown> | null> {
const doc = await this.didDocRepo.findOne({ where: { did } });
if (!doc) return null;
return this.buildW3cDocument(doc);
}

async generateChallenge(did: string): Promise<string> {
let doc = await this.didDocRepo.findOne({ where: { did } });

// Auto-onboard for demonstration if no doc exists (in reality, requires registration step)
if (!doc) {
doc = this.didDocRepo.create({
did,
userId: crypto.randomUUID(), // Mock linking to a real backend user
userId: crypto.randomUUID(),
});
}

const nonce = crypto.randomBytes(32).toString('hex');
doc.nonce = nonce;
// Set expiry 5 mins from now
doc.nonceExpiry = new Date(Date.now() + 5 * 60 * 1000);

await this.didDocRepo.save(doc);
Expand All @@ -43,13 +79,11 @@ export class DidAuthService {
}

try {
// For did:ethr:0x123..., the address is the last part
const address = did.split(':').pop();
if (!address) {
throw new UnauthorizedException('Invalid DID format');
}

// We expect the user signed the raw nonce string
const recoveredAddress = verifyMessage(doc.nonce, signature);

if (recoveredAddress.toLowerCase() !== address.toLowerCase()) {
Expand All @@ -61,7 +95,6 @@ export class DidAuthService {
doc.nonceExpiry = null;
await this.didDocRepo.save(doc);

// Return a pseudo session token for the standard auth system to consume
const sessionToken = crypto.randomBytes(32).toString('hex');
return {
message: 'DID Authentication successful',
Expand All @@ -72,4 +105,28 @@ export class DidAuthService {
throw new UnauthorizedException('Signature verification failed');
}
}

private buildW3cDocument(doc: DidDocument): Record<string, unknown> {
return {
'@context': [
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/secp256k1-2019/v1',
],
id: doc.did,
verificationMethod: doc.publicKey
? [
{
id: `${doc.did}#keys-1`,
type: 'EcdsaSecp256k1VerificationKey2019',
controller: doc.did,
publicKeyHex: doc.publicKey,
},
]
: [],
authentication: doc.publicKey ? [`${doc.did}#keys-1`] : [],
assertionMethod: doc.publicKey ? [`${doc.did}#keys-1`] : [],
created: doc.createdAt,
updated: doc.updatedAt,
};
}
}
Loading