grabbit 13ce6ae442 feat: implement complete edge device registration system
- Add hardware fingerprinting with cross-platform support
- Implement secure device registration flow with X.509 certificates
- Add WebSocket real-time communication for device status
- Create comprehensive device management dashboard
- Establish zero-trust security architecture with multi-layer protection
- Add database migrations for device registration entities
- Implement Rust edge client with hardware identification
- Add certificate management and automated provisioning system

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-13 08:46:25 +08:00

467 lines
14 KiB
TypeScript

import { Injectable, Logger, BadRequestException, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, LessThan } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
import * as forge from 'node-forge';
import { DeviceCertificate, CertificateStatus, CertificateType } from '../../entities/device-certificate.entity';
import { Device } from '../../entities/device.entity';
export interface CertificateGenerationOptions {
deviceId: string;
commonName: string;
organization?: string;
organizationalUnit?: string;
country?: string;
validityDays?: number;
keySize?: number;
algorithm?: 'RSA' | 'ECDSA';
}
export interface CertificateInfo {
serialNumber: string;
fingerprint: string;
subjectDn: string;
issuerDn: string;
validFrom: Date;
validTo: Date;
keyAlgorithm: string;
keySize: number;
signatureAlgorithm: string;
}
export interface CertificateValidationResult {
valid: boolean;
expired: boolean;
revoked: boolean;
trustChainValid: boolean;
errors: string[];
warnings: string[];
}
@Injectable()
export class CertificateService {
private readonly logger = new Logger(CertificateService.name);
private caCertificate: forge.pki.Certificate | null = null;
private caPrivateKey: forge.pki.PrivateKey | null = null;
constructor(
@InjectRepository(DeviceCertificate)
private certificateRepository: Repository<DeviceCertificate>,
@InjectRepository(Device)
private deviceRepository: Repository<Device>,
private configService: ConfigService,
) {
this.initializeCa();
}
/**
* Generates a new device certificate
*/
async generateDeviceCertificate(options: CertificateGenerationOptions): Promise<DeviceCertificate> {
try {
// Verify device exists
const device = await this.deviceRepository.findOne({
where: { id: options.deviceId },
});
if (!device) {
throw new NotFoundException(`Device ${options.deviceId} not found`);
}
// Check if device already has an active certificate
const existingCert = await this.certificateRepository.findOne({
where: {
deviceId: options.deviceId,
status: CertificateStatus.ACTIVE,
},
});
if (existingCert) {
// Revoke existing certificate
await this.revokeCertificate(existingCert.id, 'superseded');
}
// Generate key pair
const keyPair = this.generateKeyPair(options.algorithm || 'RSA', options.keySize || 2048);
// Create certificate
const cert = forge.pki.createCertificate();
cert.publicKey = keyPair.publicKey;
cert.serialNumber = this.generateSerialNumber();
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setDate(cert.validity.notBefore.getDate() + (options.validityDays || 365));
// Set subject
const subject = [
{ name: 'commonName', value: options.commonName },
{ name: 'organizationName', value: options.organization || 'Meteor Network' },
{ name: 'organizationalUnitName', value: options.organizationalUnit || 'Edge Devices' },
{ name: 'countryName', value: options.country || 'US' },
];
cert.setSubject(subject);
// Set issuer (CA)
if (this.caCertificate) {
cert.setIssuer(this.caCertificate.subject.attributes);
} else {
cert.setIssuer(subject); // Self-signed for development
}
// Add extensions
cert.setExtensions([
{
name: 'basicConstraints',
cA: false,
critical: true,
},
{
name: 'keyUsage',
digitalSignature: true,
keyEncipherment: true,
critical: true,
},
{
name: 'extKeyUsage',
clientAuth: true,
serverAuth: false,
},
{
name: 'subjectAltName',
altNames: [
{
type: 2, // DNS
value: `device-${options.deviceId}.meteor-network.local`,
},
{
type: 7, // IP
ip: '127.0.0.1',
},
],
},
{
name: 'subjectKeyIdentifier',
},
{
name: 'authorityKeyIdentifier',
keyIdentifier: (this.caCertificate?.getExtension('subjectKeyIdentifier') as any)?.subjectKeyIdentifier || false,
},
]);
// Sign certificate
const signingKey = this.caPrivateKey || keyPair.privateKey;
cert.sign(signingKey as any, forge.md.sha256.create());
// Convert to PEM format
const certificatePem = forge.pki.certificateToPem(cert);
const privateKeyPem = forge.pki.privateKeyToPem(keyPair.privateKey);
const publicKeyPem = forge.pki.publicKeyToPem(keyPair.publicKey);
// Calculate fingerprint
const fingerprint = this.calculateCertificateFingerprint(certificatePem);
// Save to database
const deviceCertificate = this.certificateRepository.create({
deviceId: options.deviceId,
serialNumber: cert.serialNumber,
fingerprint,
certificateType: CertificateType.DEVICE,
status: CertificateStatus.ACTIVE,
subjectDn: this.formatDistinguishedName(cert.subject.attributes),
issuerDn: this.formatDistinguishedName(cert.issuer.attributes),
certificatePem,
privateKeyPem,
publicKeyPem,
keyAlgorithm: options.algorithm || 'RSA',
keySize: options.keySize || 2048,
signatureAlgorithm: 'SHA256withRSA',
issuedAt: cert.validity.notBefore,
expiresAt: cert.validity.notAfter,
x509Extensions: {
key_usage: ['digitalSignature', 'keyEncipherment'],
extended_key_usage: ['clientAuth'],
subject_alt_name: [`DNS:device-${options.deviceId}.meteor-network.local`],
basic_constraints: { ca: false },
},
});
const savedCertificate = await this.certificateRepository.save(deviceCertificate);
this.logger.log(`Generated certificate ${savedCertificate.serialNumber} for device ${options.deviceId}`);
return savedCertificate;
} catch (error) {
this.logger.error('Error generating device certificate:', error);
throw new BadRequestException('Failed to generate device certificate');
}
}
/**
* Validates a certificate
*/
async validateCertificate(certificatePem: string): Promise<CertificateValidationResult> {
try {
const cert = forge.pki.certificateFromPem(certificatePem);
const now = new Date();
const errors: string[] = [];
const warnings: string[] = [];
// Check expiration
const expired = now > cert.validity.notAfter || now < cert.validity.notBefore;
if (expired) {
errors.push('Certificate is expired or not yet valid');
}
// Check if certificate is in database and revoked
const dbCertificate = await this.certificateRepository.findOne({
where: { serialNumber: cert.serialNumber },
});
const revoked = dbCertificate?.status === CertificateStatus.REVOKED;
if (revoked) {
errors.push('Certificate has been revoked');
}
// Validate certificate chain
let trustChainValid = true;
try {
if (this.caCertificate) {
const caStore = forge.pki.createCaStore([this.caCertificate]);
trustChainValid = forge.pki.verifyCertificateChain(caStore, [cert]);
}
} catch (error) {
trustChainValid = false;
errors.push('Certificate chain validation failed');
}
// Check certificate extensions
const keyUsageExt = cert.getExtension('keyUsage');
if (!keyUsageExt || !(keyUsageExt as any).digitalSignature) {
warnings.push('Certificate missing required digitalSignature key usage');
}
// Check if certificate is close to expiration (30 days)
const daysToExpiration = Math.ceil((cert.validity.notAfter.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
if (daysToExpiration <= 30 && daysToExpiration > 0) {
warnings.push(`Certificate expires in ${daysToExpiration} days`);
}
return {
valid: errors.length === 0,
expired,
revoked,
trustChainValid,
errors,
warnings,
};
} catch (error) {
this.logger.error('Error validating certificate:', error);
return {
valid: false,
expired: false,
revoked: false,
trustChainValid: false,
errors: ['Invalid certificate format'],
warnings: [],
};
}
}
/**
* Revokes a certificate
*/
async revokeCertificate(certificateId: string, reason: string): Promise<void> {
const certificate = await this.certificateRepository.findOne({
where: { id: certificateId },
});
if (!certificate) {
throw new NotFoundException(`Certificate ${certificateId} not found`);
}
if (certificate.status === CertificateStatus.REVOKED) {
throw new BadRequestException('Certificate is already revoked');
}
certificate.status = CertificateStatus.REVOKED;
certificate.revokedAt = new Date();
certificate.revocationReason = reason;
await this.certificateRepository.save(certificate);
this.logger.warn(`Certificate ${certificate.serialNumber} revoked: ${reason}`);
}
/**
* Renews a certificate before expiration
*/
async renewCertificate(certificateId: string): Promise<DeviceCertificate> {
const existingCert = await this.certificateRepository.findOne({
where: { id: certificateId },
relations: ['device'],
});
if (!existingCert) {
throw new NotFoundException(`Certificate ${certificateId} not found`);
}
const daysToExpiration = Math.ceil((existingCert.expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
if (daysToExpiration > 30) {
throw new BadRequestException('Certificate renewal is only allowed within 30 days of expiration');
}
// Generate new certificate with same parameters
const newCertificate = await this.generateDeviceCertificate({
deviceId: existingCert.deviceId,
commonName: this.extractCommonName(existingCert.subjectDn),
validityDays: 365,
keySize: existingCert.keySize,
algorithm: existingCert.keyAlgorithm as 'RSA' | 'ECDSA',
});
this.logger.log(`Certificate ${existingCert.serialNumber} renewed as ${newCertificate.serialNumber}`);
return newCertificate;
}
/**
* Gets certificates expiring soon
*/
async getCertificatesExpiringSoon(days: number = 30): Promise<DeviceCertificate[]> {
const expirationDate = new Date();
expirationDate.setDate(expirationDate.getDate() + days);
return this.certificateRepository.find({
where: {
status: CertificateStatus.ACTIVE,
expiresAt: LessThan(expirationDate),
},
relations: ['device'],
});
}
/**
* Gets certificate information
*/
async getCertificateInfo(certificatePem: string): Promise<CertificateInfo> {
try {
const cert = forge.pki.certificateFromPem(certificatePem);
return {
serialNumber: cert.serialNumber,
fingerprint: this.calculateCertificateFingerprint(certificatePem),
subjectDn: this.formatDistinguishedName(cert.subject.attributes),
issuerDn: this.formatDistinguishedName(cert.issuer.attributes),
validFrom: cert.validity.notBefore,
validTo: cert.validity.notAfter,
keyAlgorithm: this.getKeyAlgorithm(cert.publicKey),
keySize: this.getKeySize(cert.publicKey),
signatureAlgorithm: cert.signatureOid,
};
} catch (error) {
throw new BadRequestException('Invalid certificate format');
}
}
/**
* Updates certificate usage statistics
*/
async recordCertificateUsage(serialNumber: string): Promise<void> {
await this.certificateRepository.increment(
{ serialNumber },
'usageCount',
1,
);
await this.certificateRepository.update(
{ serialNumber },
{ lastUsedAt: new Date() },
);
}
/**
* Initializes CA certificate and key
*/
private initializeCa(): void {
try {
const caCertPem = this.configService.get<string>('DEVICE_CA_CERT');
const caKeyPem = this.configService.get<string>('DEVICE_CA_KEY');
if (caCertPem && caKeyPem) {
this.caCertificate = forge.pki.certificateFromPem(caCertPem);
this.caPrivateKey = forge.pki.privateKeyFromPem(caKeyPem);
this.logger.log('CA certificate and key loaded successfully');
} else {
this.logger.warn('CA certificate and key not configured, using self-signed certificates');
}
} catch (error) {
this.logger.error('Failed to load CA certificate and key:', error);
}
}
/**
* Generates a key pair
*/
private generateKeyPair(algorithm: string, keySize: number): forge.pki.KeyPair {
if (algorithm === 'RSA') {
return forge.pki.rsa.generateKeyPair({
bits: keySize,
workers: -1, // Use available web workers
});
} else if (algorithm === 'ECDSA') {
// Note: node-forge has limited ECDSA support
throw new BadRequestException('ECDSA key generation not supported in current implementation');
} else {
throw new BadRequestException(`Unsupported algorithm: ${algorithm}`);
}
}
/**
* Generates a unique serial number
*/
private generateSerialNumber(): string {
return crypto.randomBytes(16).toString('hex');
}
/**
* Calculates certificate fingerprint (SHA-256)
*/
private calculateCertificateFingerprint(certificatePem: string): string {
return crypto.createHash('sha256').update(certificatePem).digest('hex');
}
/**
* Formats distinguished name
*/
private formatDistinguishedName(attributes: any[]): string {
return attributes
.map(attr => `${attr.name}=${attr.value}`)
.join(', ');
}
/**
* Extracts common name from DN
*/
private extractCommonName(dn: string): string {
const match = dn.match(/commonName=([^,]+)/);
return match ? match[1] : 'Unknown';
}
/**
* Gets key algorithm from public key
*/
private getKeyAlgorithm(publicKey: forge.pki.PublicKey): string {
// In node-forge, RSA keys have 'n' property
return (publicKey as any).n ? 'RSA' : 'Unknown';
}
/**
* Gets key size from public key
*/
private getKeySize(publicKey: forge.pki.PublicKey): number {
const rsaKey = publicKey as any;
return rsaKey.n ? rsaKey.n.bitLength() : 0;
}
}