This commit introduces a complete transparency infrastructure including: Core Transparency Modules: - AuditLog: Immutable, cryptographically-linked audit trail for all actions - EventStream: Real-time SSE streaming and webhook support - TransparencyDashboard: Aggregated metrics and system health monitoring - DigitalSignatures: Cryptographic verification for handoffs and certificates API Endpoints: - /api/transparency/dashboard - Full platform metrics - /api/transparency/audit - Query and log audit entries - /api/transparency/events - SSE stream and event history - /api/transparency/webhooks - Webhook management - /api/transparency/signatures - Digital signature operations - /api/transparency/certificate/[plantId] - Plant authenticity certificates - /api/transparency/export - Multi-format data export - /api/transparency/report - Compliance reporting - /api/transparency/health - System health checks Features: - Immutable audit logging with chain integrity verification - Real-time event streaming via Server-Sent Events - Webhook support with HMAC signature verification - Digital signatures for transport handoffs and ownership transfers - Certificate of Authenticity generation for plants - Multi-format data export (JSON, CSV, summary) - Public transparency portal at /transparency - System health monitoring for all components Documentation: - Comprehensive TRANSPARENCY.md guide with API examples
617 lines
15 KiB
TypeScript
617 lines
15 KiB
TypeScript
/**
|
|
* Digital Signatures System for LocalGreenChain
|
|
*
|
|
* Provides cryptographic verification for:
|
|
* - Transport handoffs
|
|
* - Plant ownership transfers
|
|
* - Data integrity verification
|
|
* - Multi-party attestations
|
|
*/
|
|
|
|
import * as crypto from 'crypto';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
// Signature Types
|
|
export type SignatureType =
|
|
| 'OWNERSHIP_TRANSFER'
|
|
| 'TRANSPORT_HANDOFF'
|
|
| 'HARVEST_ATTESTATION'
|
|
| 'QUALITY_CERTIFICATION'
|
|
| 'ORIGIN_VERIFICATION'
|
|
| 'DATA_INTEGRITY';
|
|
|
|
export interface KeyPair {
|
|
publicKey: string;
|
|
privateKey: string;
|
|
algorithm: 'RSA' | 'ECDSA';
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface SignatureIdentity {
|
|
id: string;
|
|
name: string;
|
|
publicKey: string;
|
|
type: 'GROWER' | 'TRANSPORTER' | 'CERTIFIER' | 'CONSUMER' | 'SYSTEM';
|
|
verified: boolean;
|
|
createdAt: string;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export interface DigitalSignature {
|
|
id: string;
|
|
type: SignatureType;
|
|
signerId: string;
|
|
signerPublicKey: string;
|
|
timestamp: string;
|
|
data: string; // JSON stringified data that was signed
|
|
signature: string; // Base64 encoded signature
|
|
algorithm: string;
|
|
resourceType: string;
|
|
resourceId: string;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export interface MultiSignature {
|
|
id: string;
|
|
type: SignatureType;
|
|
resourceType: string;
|
|
resourceId: string;
|
|
requiredSigners: string[];
|
|
signatures: DigitalSignature[];
|
|
threshold: number;
|
|
complete: boolean;
|
|
createdAt: string;
|
|
completedAt?: string;
|
|
expiresAt?: string;
|
|
}
|
|
|
|
export interface SignatureVerification {
|
|
valid: boolean;
|
|
signerId: string;
|
|
timestamp: string;
|
|
resourceId: string;
|
|
errors?: string[];
|
|
}
|
|
|
|
export interface CertificateOfAuthenticity {
|
|
id: string;
|
|
plantId: string;
|
|
plantName: string;
|
|
variety: string;
|
|
origin: {
|
|
grower: string;
|
|
location: string;
|
|
registeredAt: string;
|
|
};
|
|
lineage: {
|
|
generation: number;
|
|
parentId?: string;
|
|
childCount: number;
|
|
};
|
|
signatures: Array<{
|
|
type: SignatureType;
|
|
signer: string;
|
|
timestamp: string;
|
|
verified: boolean;
|
|
}>;
|
|
transportHistory: Array<{
|
|
from: string;
|
|
to: string;
|
|
date: string;
|
|
verified: boolean;
|
|
}>;
|
|
certifications: string[];
|
|
qrCode?: string;
|
|
generatedAt: string;
|
|
hash: string;
|
|
}
|
|
|
|
class SignatureManager {
|
|
private identities: Map<string, SignatureIdentity> = new Map();
|
|
private signatures: Map<string, DigitalSignature> = new Map();
|
|
private multiSignatures: Map<string, MultiSignature> = new Map();
|
|
private dataDir: string;
|
|
private dataFile: string;
|
|
|
|
constructor() {
|
|
this.dataDir = path.join(process.cwd(), 'data');
|
|
this.dataFile = path.join(this.dataDir, 'signatures.json');
|
|
this.load();
|
|
}
|
|
|
|
private load(): void {
|
|
try {
|
|
if (fs.existsSync(this.dataFile)) {
|
|
const data = JSON.parse(fs.readFileSync(this.dataFile, 'utf-8'));
|
|
|
|
if (data.identities) {
|
|
data.identities.forEach((id: SignatureIdentity) => {
|
|
this.identities.set(id.id, id);
|
|
});
|
|
}
|
|
|
|
if (data.signatures) {
|
|
data.signatures.forEach((sig: DigitalSignature) => {
|
|
this.signatures.set(sig.id, sig);
|
|
});
|
|
}
|
|
|
|
if (data.multiSignatures) {
|
|
data.multiSignatures.forEach((ms: MultiSignature) => {
|
|
this.multiSignatures.set(ms.id, ms);
|
|
});
|
|
}
|
|
|
|
console.log(`[SignatureManager] Loaded ${this.identities.size} identities, ${this.signatures.size} signatures`);
|
|
}
|
|
} catch (error) {
|
|
console.error('[SignatureManager] Error loading data:', error);
|
|
}
|
|
}
|
|
|
|
private save(): void {
|
|
try {
|
|
if (!fs.existsSync(this.dataDir)) {
|
|
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
}
|
|
|
|
const data = {
|
|
identities: Array.from(this.identities.values()),
|
|
signatures: Array.from(this.signatures.values()),
|
|
multiSignatures: Array.from(this.multiSignatures.values()),
|
|
savedAt: new Date().toISOString()
|
|
};
|
|
|
|
fs.writeFileSync(this.dataFile, JSON.stringify(data, null, 2));
|
|
} catch (error) {
|
|
console.error('[SignatureManager] Error saving data:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a new key pair
|
|
*/
|
|
generateKeyPair(algorithm: 'RSA' | 'ECDSA' = 'ECDSA'): KeyPair {
|
|
if (algorithm === 'RSA') {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
});
|
|
|
|
return {
|
|
publicKey,
|
|
privateKey,
|
|
algorithm: 'RSA',
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
} else {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
|
|
namedCurve: 'secp256k1',
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
});
|
|
|
|
return {
|
|
publicKey,
|
|
privateKey,
|
|
algorithm: 'ECDSA',
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a new identity
|
|
*/
|
|
registerIdentity(
|
|
name: string,
|
|
type: SignatureIdentity['type'],
|
|
publicKey: string,
|
|
metadata?: Record<string, any>
|
|
): SignatureIdentity {
|
|
const id = `id_${crypto.randomBytes(16).toString('hex')}`;
|
|
|
|
const identity: SignatureIdentity = {
|
|
id,
|
|
name,
|
|
publicKey,
|
|
type,
|
|
verified: false,
|
|
createdAt: new Date().toISOString(),
|
|
metadata
|
|
};
|
|
|
|
this.identities.set(id, identity);
|
|
this.save();
|
|
|
|
return identity;
|
|
}
|
|
|
|
/**
|
|
* Get identity by ID
|
|
*/
|
|
getIdentity(id: string): SignatureIdentity | undefined {
|
|
return this.identities.get(id);
|
|
}
|
|
|
|
/**
|
|
* Get all identities
|
|
*/
|
|
getAllIdentities(): SignatureIdentity[] {
|
|
return Array.from(this.identities.values());
|
|
}
|
|
|
|
/**
|
|
* Verify an identity
|
|
*/
|
|
verifyIdentity(id: string): boolean {
|
|
const identity = this.identities.get(id);
|
|
if (identity) {
|
|
identity.verified = true;
|
|
this.save();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sign data
|
|
*/
|
|
sign(
|
|
type: SignatureType,
|
|
resourceType: string,
|
|
resourceId: string,
|
|
data: any,
|
|
privateKey: string,
|
|
signerId: string,
|
|
metadata?: Record<string, any>
|
|
): DigitalSignature {
|
|
const dataString = JSON.stringify(data);
|
|
|
|
// Create signature
|
|
const sign = crypto.createSign('SHA256');
|
|
sign.update(dataString);
|
|
const signature = sign.sign(privateKey, 'base64');
|
|
|
|
const id = `sig_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
|
|
const identity = this.identities.get(signerId);
|
|
|
|
const digitalSignature: DigitalSignature = {
|
|
id,
|
|
type,
|
|
signerId,
|
|
signerPublicKey: identity?.publicKey || '',
|
|
timestamp: new Date().toISOString(),
|
|
data: dataString,
|
|
signature,
|
|
algorithm: 'SHA256',
|
|
resourceType,
|
|
resourceId,
|
|
metadata
|
|
};
|
|
|
|
this.signatures.set(id, digitalSignature);
|
|
this.save();
|
|
|
|
return digitalSignature;
|
|
}
|
|
|
|
/**
|
|
* Verify a signature
|
|
*/
|
|
verify(signatureId: string): SignatureVerification {
|
|
const sig = this.signatures.get(signatureId);
|
|
|
|
if (!sig) {
|
|
return {
|
|
valid: false,
|
|
signerId: '',
|
|
timestamp: '',
|
|
resourceId: '',
|
|
errors: ['Signature not found']
|
|
};
|
|
}
|
|
|
|
try {
|
|
const verify = crypto.createVerify('SHA256');
|
|
verify.update(sig.data);
|
|
const isValid = verify.verify(sig.signerPublicKey, sig.signature, 'base64');
|
|
|
|
return {
|
|
valid: isValid,
|
|
signerId: sig.signerId,
|
|
timestamp: sig.timestamp,
|
|
resourceId: sig.resourceId,
|
|
errors: isValid ? undefined : ['Signature verification failed']
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
valid: false,
|
|
signerId: sig.signerId,
|
|
timestamp: sig.timestamp,
|
|
resourceId: sig.resourceId,
|
|
errors: [`Verification error: ${error}`]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify data with signature directly
|
|
*/
|
|
verifyData(data: any, signature: string, publicKey: string): boolean {
|
|
try {
|
|
const dataString = JSON.stringify(data);
|
|
const verify = crypto.createVerify('SHA256');
|
|
verify.update(dataString);
|
|
return verify.verify(publicKey, signature, 'base64');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a multi-signature request
|
|
*/
|
|
createMultiSignature(
|
|
type: SignatureType,
|
|
resourceType: string,
|
|
resourceId: string,
|
|
requiredSigners: string[],
|
|
threshold?: number,
|
|
expiresIn?: number
|
|
): MultiSignature {
|
|
const id = `msig_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
|
|
|
|
const multiSig: MultiSignature = {
|
|
id,
|
|
type,
|
|
resourceType,
|
|
resourceId,
|
|
requiredSigners,
|
|
signatures: [],
|
|
threshold: threshold || requiredSigners.length,
|
|
complete: false,
|
|
createdAt: new Date().toISOString(),
|
|
expiresAt: expiresIn ? new Date(Date.now() + expiresIn).toISOString() : undefined
|
|
};
|
|
|
|
this.multiSignatures.set(id, multiSig);
|
|
this.save();
|
|
|
|
return multiSig;
|
|
}
|
|
|
|
/**
|
|
* Add a signature to a multi-signature request
|
|
*/
|
|
addToMultiSignature(
|
|
multiSigId: string,
|
|
signature: DigitalSignature
|
|
): MultiSignature | null {
|
|
const multiSig = this.multiSignatures.get(multiSigId);
|
|
|
|
if (!multiSig) return null;
|
|
|
|
// Check if signer is required
|
|
if (!multiSig.requiredSigners.includes(signature.signerId)) {
|
|
return null;
|
|
}
|
|
|
|
// Check if already signed
|
|
if (multiSig.signatures.some(s => s.signerId === signature.signerId)) {
|
|
return multiSig;
|
|
}
|
|
|
|
// Check expiration
|
|
if (multiSig.expiresAt && new Date(multiSig.expiresAt) < new Date()) {
|
|
return null;
|
|
}
|
|
|
|
multiSig.signatures.push(signature);
|
|
|
|
// Check if complete
|
|
if (multiSig.signatures.length >= multiSig.threshold) {
|
|
multiSig.complete = true;
|
|
multiSig.completedAt = new Date().toISOString();
|
|
}
|
|
|
|
this.save();
|
|
|
|
return multiSig;
|
|
}
|
|
|
|
/**
|
|
* Get multi-signature status
|
|
*/
|
|
getMultiSignature(id: string): MultiSignature | undefined {
|
|
return this.multiSignatures.get(id);
|
|
}
|
|
|
|
/**
|
|
* Get signatures for a resource
|
|
*/
|
|
getSignaturesForResource(resourceType: string, resourceId: string): DigitalSignature[] {
|
|
return Array.from(this.signatures.values()).filter(
|
|
s => s.resourceType === resourceType && s.resourceId === resourceId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate a Certificate of Authenticity
|
|
*/
|
|
async generateCertificate(plantId: string): Promise<CertificateOfAuthenticity | null> {
|
|
try {
|
|
// Load plant data
|
|
const chainFile = path.join(this.dataDir, 'plantchain.json');
|
|
if (!fs.existsSync(chainFile)) {
|
|
return null;
|
|
}
|
|
|
|
const chainData = JSON.parse(fs.readFileSync(chainFile, 'utf-8'));
|
|
const chain = chainData.chain || [];
|
|
|
|
// Find plant
|
|
const plantBlock = chain.find((b: any) => b.plant?.id === plantId);
|
|
if (!plantBlock) {
|
|
return null;
|
|
}
|
|
|
|
const plant = plantBlock.plant;
|
|
|
|
// Get signatures for this plant
|
|
const plantSignatures = this.getSignaturesForResource('plant', plantId);
|
|
|
|
// Build certificate
|
|
const certificate: CertificateOfAuthenticity = {
|
|
id: `cert_${crypto.randomBytes(16).toString('hex')}`,
|
|
plantId,
|
|
plantName: plant.name || 'Unknown',
|
|
variety: plant.variety || 'Unknown',
|
|
origin: {
|
|
grower: plant.ownerId || 'Unknown',
|
|
location: plant.location?.region || 'Unknown',
|
|
registeredAt: plantBlock.timestamp
|
|
},
|
|
lineage: {
|
|
generation: plant.generation || 1,
|
|
parentId: plant.parentId,
|
|
childCount: plant.childPlants?.length || 0
|
|
},
|
|
signatures: plantSignatures.map(sig => ({
|
|
type: sig.type,
|
|
signer: sig.signerId,
|
|
timestamp: sig.timestamp,
|
|
verified: this.verify(sig.id).valid
|
|
})),
|
|
transportHistory: [],
|
|
certifications: [],
|
|
generatedAt: new Date().toISOString(),
|
|
hash: ''
|
|
};
|
|
|
|
// Calculate certificate hash
|
|
certificate.hash = crypto
|
|
.createHash('sha256')
|
|
.update(JSON.stringify({
|
|
plantId: certificate.plantId,
|
|
origin: certificate.origin,
|
|
lineage: certificate.lineage,
|
|
signatures: certificate.signatures
|
|
}))
|
|
.digest('hex');
|
|
|
|
return certificate;
|
|
} catch (error) {
|
|
console.error('[SignatureManager] Error generating certificate:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a certificate
|
|
*/
|
|
verifyCertificate(certificate: CertificateOfAuthenticity): { valid: boolean; errors: string[] } {
|
|
const errors: string[] = [];
|
|
|
|
// Verify hash
|
|
const expectedHash = crypto
|
|
.createHash('sha256')
|
|
.update(JSON.stringify({
|
|
plantId: certificate.plantId,
|
|
origin: certificate.origin,
|
|
lineage: certificate.lineage,
|
|
signatures: certificate.signatures
|
|
}))
|
|
.digest('hex');
|
|
|
|
if (certificate.hash !== expectedHash) {
|
|
errors.push('Certificate hash mismatch - data may have been tampered');
|
|
}
|
|
|
|
// Verify each signature
|
|
for (const sig of certificate.signatures) {
|
|
if (!sig.verified) {
|
|
errors.push(`Signature from ${sig.signer} is not verified`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a transport handoff signature
|
|
*/
|
|
createTransportHandoff(
|
|
plantId: string,
|
|
fromParty: string,
|
|
toParty: string,
|
|
location: { lat: number; lng: number },
|
|
fromPrivateKey: string
|
|
): DigitalSignature {
|
|
const handoffData = {
|
|
plantId,
|
|
fromParty,
|
|
toParty,
|
|
location,
|
|
timestamp: new Date().toISOString(),
|
|
type: 'TRANSPORT_HANDOFF'
|
|
};
|
|
|
|
return this.sign(
|
|
'TRANSPORT_HANDOFF',
|
|
'transport',
|
|
plantId,
|
|
handoffData,
|
|
fromPrivateKey,
|
|
fromParty,
|
|
{ toParty, location }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get statistics
|
|
*/
|
|
getStats(): {
|
|
totalIdentities: number;
|
|
verifiedIdentities: number;
|
|
totalSignatures: number;
|
|
signaturesByType: Record<string, number>;
|
|
pendingMultiSigs: number;
|
|
completedMultiSigs: number;
|
|
} {
|
|
const signaturesByType: Record<string, number> = {};
|
|
|
|
for (const sig of this.signatures.values()) {
|
|
signaturesByType[sig.type] = (signaturesByType[sig.type] || 0) + 1;
|
|
}
|
|
|
|
const multiSigArray = Array.from(this.multiSignatures.values());
|
|
|
|
return {
|
|
totalIdentities: this.identities.size,
|
|
verifiedIdentities: Array.from(this.identities.values()).filter(i => i.verified).length,
|
|
totalSignatures: this.signatures.size,
|
|
signaturesByType,
|
|
pendingMultiSigs: multiSigArray.filter(ms => !ms.complete).length,
|
|
completedMultiSigs: multiSigArray.filter(ms => ms.complete).length
|
|
};
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
let signatureManagerInstance: SignatureManager | null = null;
|
|
|
|
export function getSignatureManager(): SignatureManager {
|
|
if (!signatureManagerInstance) {
|
|
signatureManagerInstance = new SignatureManager();
|
|
}
|
|
return signatureManagerInstance;
|
|
}
|
|
|
|
export default SignatureManager;
|