Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…, permission enforcement Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
…ule loading Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
…gorithm Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements a comprehensive security enforcement layer for the ObjectStack microkernel, addressing critical gaps identified in the MICROKERNEL_ASSESSMENT.md. The implementation adds cryptographic plugin signature verification, Zod-based configuration validation, and capability-based permission enforcement.
Changes:
- Adds three core security components (PluginSignatureVerifier, PluginConfigValidator, PluginPermissionEnforcer) with runtime enforcement
- Implements SecurePluginContext wrapper for transparent permission checking during plugin operations
- Updates plugin-loader.ts TODO comments to reference new security implementations
- Provides comprehensive security documentation (PLUGIN_SECURITY_GUIDE.md) and gap analysis (MICROKERNEL_ASSESSMENT.md in Chinese)
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/security/plugin-signature-verifier.ts | RSA/ECDSA signature verification with trusted publisher registry and cross-platform crypto support |
| packages/core/src/security/plugin-config-validator.ts | Zod schema validation with detailed error reporting and partial config support |
| packages/core/src/security/plugin-permission-enforcer.ts | Capability-based access control for services, hooks, filesystem, and network |
| packages/core/src/security/index.ts | Security module exports |
| packages/core/src/security/plugin-permission-enforcer.test.ts | Test suite for permission enforcement (17 test cases) |
| packages/core/src/security/plugin-config-validator.test.ts | Test suite for config validation (13 test cases) |
| packages/core/src/plugin-loader.ts | Updates TODO comments to reference new security implementations |
| packages/core/src/index.ts | Exports security module |
| PLUGIN_SECURITY_GUIDE.md | Comprehensive security best practices guide (533 lines) |
| MICROKERNEL_ASSESSMENT.md | Detailed microkernel gap analysis and improvement roadmap in Chinese (656 lines) |
| @@ -0,0 +1,242 @@ | |||
| import { describe, it, expect, beforeEach } from 'vitest'; | |||
| import { PluginPermissionEnforcer, SecurePluginContext } from '../plugin-permission-enforcer.js'; | |||
There was a problem hiding this comment.
The import path uses a relative parent directory path (../plugin-permission-enforcer.js) but the test file is in the same directory as the implementation file. This should be ./plugin-permission-enforcer.js instead.
| import { PluginPermissionEnforcer, SecurePluginContext } from '../plugin-permission-enforcer.js'; | |
| import { PluginPermissionEnforcer, SecurePluginContext } from './plugin-permission-enforcer.js'; |
| @@ -0,0 +1,276 @@ | |||
| import { describe, it, expect, beforeEach } from 'vitest'; | |||
| import { z } from 'zod'; | |||
| import { PluginConfigValidator } from '../plugin-config-validator.js'; | |||
There was a problem hiding this comment.
The import path uses a relative parent directory path (../plugin-config-validator.js) but the test file is in the same directory as the implementation file. This should be ./plugin-config-validator.js instead.
| import { PluginConfigValidator } from '../plugin-config-validator.js'; | |
| import { PluginConfigValidator } from './plugin-config-validator.js'; |
| private checkFileRead(capabilities: PluginCapability[], _path: string): boolean { | ||
| // Check if plugin has capability to read this file | ||
| return capabilities.some(cap => { | ||
| const protocolId = cap.protocol.id; | ||
|
|
||
| // Check for file read capability | ||
| if (protocolId.includes('protocol.filesystem.read')) { | ||
| // TODO: Add path pattern matching | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
| } | ||
|
|
||
| private checkFileWrite(capabilities: PluginCapability[], _path: string): boolean { | ||
| // Check if plugin has capability to write this file | ||
| return capabilities.some(cap => { | ||
| const protocolId = cap.protocol.id; | ||
|
|
||
| // Check for file write capability | ||
| if (protocolId.includes('protocol.filesystem.write')) { | ||
| // TODO: Add path pattern matching | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
| } |
There was a problem hiding this comment.
File read and write permission checks ignore the path parameter, allowing plugins with filesystem.read capability to read ANY file on the system. This is a critical security vulnerability. The TODO comment indicates path pattern matching is needed, but until implemented, plugins can bypass intended restrictions. Consider either implementing path restrictions immediately or failing permission checks until proper path validation is in place.
| private checkNetworkAccess(capabilities: PluginCapability[], _url: string): boolean { | ||
| // Check if plugin has capability to access this URL | ||
| return capabilities.some(cap => { | ||
| const protocolId = cap.protocol.id; | ||
|
|
||
| // Check for network capability | ||
| if (protocolId.includes('protocol.network')) { | ||
| // TODO: Add URL pattern matching | ||
| return true; | ||
| } | ||
|
|
There was a problem hiding this comment.
Network permission check ignores the URL parameter, allowing plugins with protocol.network capability to access ANY URL. This defeats the purpose of granular permission controls and could allow plugins to exfiltrate data to arbitrary external servers. The TODO comment indicates URL pattern matching is needed - until implemented, consider either restricting this capability or implementing URL validation.
| private checkNetworkAccess(capabilities: PluginCapability[], _url: string): boolean { | |
| // Check if plugin has capability to access this URL | |
| return capabilities.some(cap => { | |
| const protocolId = cap.protocol.id; | |
| // Check for network capability | |
| if (protocolId.includes('protocol.network')) { | |
| // TODO: Add URL pattern matching | |
| return true; | |
| } | |
| private checkNetworkAccess(capabilities: PluginCapability[], url: string): boolean { | |
| // Validate and normalize the requested URL first | |
| let normalizedUrl: string; | |
| try { | |
| const parsed = new URL(url); | |
| // Use the full href for prefix comparison | |
| normalizedUrl = parsed.href; | |
| } catch { | |
| // If the URL is not valid, deny access | |
| return false; | |
| } | |
| // Check if plugin has capability to access this URL | |
| return capabilities.some(cap => { | |
| const protocolId = cap.protocol.id; | |
| // Enforce network capability with URL prefix scoping: | |
| // - Exact "protocol.network" (no suffix) does NOT grant blanket access. | |
| // - "protocol.network:<prefix>" allows URLs starting with <prefix>. | |
| const prefixMarker = 'protocol.network:'; | |
| if (protocolId === 'protocol.network') { | |
| // Unscoped network capability is considered insufficient for security. | |
| return false; | |
| } | |
| if (protocolId.startsWith(prefixMarker)) { | |
| const allowedPrefix = protocolId.slice(prefixMarker.length); | |
| if (!allowedPrefix) { | |
| // Empty prefix is not allowed | |
| return false; | |
| } | |
| // Compare using simple prefix matching on the normalized URL | |
| return normalizedUrl.startsWith(allowedPrefix); | |
| } | |
| // Any other protocol ids do not confer network access |
| private checkHookAccess(capabilities: PluginCapability[], hookName: string): boolean { | ||
| // Check if plugin has capability to trigger this hook | ||
| return capabilities.some(cap => { | ||
| const protocolId = cap.protocol.id; | ||
|
|
||
| // Check for wildcard hook access | ||
| if (protocolId.includes('protocol.hook.all')) { | ||
| return true; | ||
| } | ||
|
|
||
| // Check for specific hook protocol | ||
| if (protocolId.includes(`protocol.hook.${hookName}`)) { | ||
| return true; | ||
| } | ||
|
|
||
| // Check for hook category match | ||
| const hookCategory = hookName.split(':')[0]; | ||
| if (protocolId.includes(`protocol.hook.${hookCategory}`)) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Using .includes() for protocol ID matching is unsafe because it can match substrings anywhere in the string. For example, a malicious capability could include 'protocol.hook.all' as part of a longer string and bypass restrictions. Consider using exact string matching or proper prefix/suffix matching to prevent capability bypass attacks.
| // Serialize plugin code for hashing | ||
| // Include init, start, destroy functions | ||
| const parts: string[] = [ | ||
| plugin.name, | ||
| plugin.version, | ||
| plugin.init.toString(), | ||
| ]; | ||
|
|
||
| if (plugin.start) { | ||
| parts.push(plugin.start.toString()); | ||
| } | ||
|
|
||
| if (plugin.destroy) { | ||
| parts.push(plugin.destroy.toString()); | ||
| } | ||
|
|
There was a problem hiding this comment.
Using .toString() on functions for signature verification is unreliable because function representations can vary across JavaScript engines and bundlers. Minification, transpilation, or different JavaScript engines can produce different string representations for the same code, causing signature verification to fail. Consider signing the actual source code file contents or a hash of the bundled output instead.
| // Serialize plugin code for hashing | |
| // Include init, start, destroy functions | |
| const parts: string[] = [ | |
| plugin.name, | |
| plugin.version, | |
| plugin.init.toString(), | |
| ]; | |
| if (plugin.start) { | |
| parts.push(plugin.start.toString()); | |
| } | |
| if (plugin.destroy) { | |
| parts.push(plugin.destroy.toString()); | |
| } | |
| // Serialize plugin metadata for hashing | |
| // Use stable metadata (names and arity) instead of function source code | |
| const parts: string[] = [ | |
| plugin.name, | |
| plugin.version, | |
| JSON.stringify({ | |
| role: 'init', | |
| name: plugin.init.name, | |
| length: plugin.init.length, | |
| }), | |
| ]; | |
| if (plugin.start) { | |
| parts.push( | |
| JSON.stringify({ | |
| role: 'start', | |
| name: plugin.start.name, | |
| length: plugin.start.length, | |
| }), | |
| ); | |
| } | |
| if (plugin.destroy) { | |
| parts.push( | |
| JSON.stringify({ | |
| role: 'destroy', | |
| name: plugin.destroy.name, | |
| length: plugin.destroy.length, | |
| }), | |
| ); | |
| } |
| _data: string, | ||
| _signature: string, | ||
| _publicKey: string | ||
| ): Promise<boolean> { | ||
| // Browser implementation using SubtleCrypto | ||
| // TODO: Implement SubtleCrypto-based verification | ||
| this.logger.warn('Browser signature verification not yet implemented'); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Browser signature verification always returns false, which means signature verification silently fails in browser environments. If strict mode is enabled, this will prevent all plugins from loading in browsers. Consider either properly implementing SubtleCrypto verification or throwing an explicit error that browser environments don't support signature verification when it's required.
| _data: string, | |
| _signature: string, | |
| _publicKey: string | |
| ): Promise<boolean> { | |
| // Browser implementation using SubtleCrypto | |
| // TODO: Implement SubtleCrypto-based verification | |
| this.logger.warn('Browser signature verification not yet implemented'); | |
| return false; | |
| } | |
| data: string, | |
| signature: string, | |
| publicKey: string | |
| ): Promise<boolean> { | |
| // Browser implementation using Web Crypto (SubtleCrypto) | |
| const cryptoObj = (globalThis as unknown as { crypto?: Crypto }).crypto; | |
| if (!cryptoObj || !cryptoObj.subtle) { | |
| const message = 'Browser SubtleCrypto not available for signature verification'; | |
| this.logger.error(message); | |
| throw new Error(message); | |
| } | |
| try { | |
| const algorithm = this.getBrowserAlgorithmParams(); | |
| // Import public key from PEM | |
| const keyData = this.pemToArrayBuffer(publicKey); | |
| const key = await cryptoObj.subtle.importKey( | |
| 'spki', | |
| keyData, | |
| algorithm, | |
| false, | |
| ['verify'] | |
| ); | |
| const dataBuffer = this.textToArrayBuffer(data); | |
| const signatureBuffer = this.base64ToArrayBuffer(signature); | |
| const verified = await cryptoObj.subtle.verify( | |
| algorithm, | |
| key, | |
| signatureBuffer, | |
| dataBuffer | |
| ); | |
| return verified; | |
| } catch (error) { | |
| this.logger.error('Browser signature verification failed', error as Error); | |
| return false; | |
| } | |
| } | |
| /** | |
| * Map configured algorithm to Web Crypto parameters for browser verification. | |
| */ | |
| private getBrowserAlgorithmParams(): CryptoKeyImportParams & Algorithm { | |
| if (this.config.algorithm === 'ES256') { | |
| return { | |
| name: 'ECDSA', | |
| namedCurve: 'P-256', | |
| hash: { name: 'SHA-256' }, | |
| } as EcKeyImportParams & EcdsaParams; | |
| } | |
| // Default to RS256 (RSASSA-PKCS1-v1_5 with SHA-256) | |
| return { | |
| name: 'RSASSA-PKCS1-v1_5', | |
| hash: { name: 'SHA-256' }, | |
| } as RsaHashedImportParams & Algorithm; | |
| } | |
| /** | |
| * Convert UTF-8 string to ArrayBuffer for Web Crypto. | |
| */ | |
| private textToArrayBuffer(text: string): ArrayBuffer { | |
| if (typeof TextEncoder !== 'undefined') { | |
| return new TextEncoder().encode(text).buffer; | |
| } | |
| // Fallback encoding if TextEncoder is not available (very old environments) | |
| const buf = new Uint8Array(text.length); | |
| for (let i = 0; i < text.length; i++) { | |
| buf[i] = text.charCodeAt(i) & 0xff; | |
| } | |
| return buf.buffer; | |
| } | |
| /** | |
| * Decode a base64 string into an ArrayBuffer. | |
| */ | |
| private base64ToArrayBuffer(base64: string): ArrayBuffer { | |
| // Normalize potential URL-safe base64 variants | |
| const normalized = base64.replace(/-/g, '+').replace(/_/g, '/'); | |
| const binaryString = globalThis.atob | |
| ? globalThis.atob(normalized) | |
| : Buffer.from(normalized, 'base64').toString('binary'); | |
| const len = binaryString.length; | |
| const bytes = new Uint8Array(len); | |
| for (let i = 0; i < len; i++) { | |
| bytes[i] = binaryString.charCodeAt(i); | |
| } | |
| return bytes.buffer; | |
| } | |
| /** | |
| * Convert a PEM-formatted public key to raw key bytes (SPKI) as ArrayBuffer. | |
| */ | |
| private pemToArrayBuffer(pem: string): ArrayBuffer { | |
| // Remove PEM header/footer and whitespace | |
| const cleaned = pem | |
| .replace(/-----BEGIN PUBLIC KEY-----/g, '') | |
| .replace(/-----END PUBLIC KEY-----/g, '') | |
| .replace(/\s+/g, ''); | |
| return this.base64ToArrayBuffer(cleaned); | |
| } |
| private checkServiceAccess(capabilities: PluginCapability[], serviceName: string): boolean { | ||
| // Check if plugin has capability to access this service | ||
| return capabilities.some(cap => { | ||
| const protocolId = cap.protocol.id; | ||
|
|
||
| // Check for wildcard service access | ||
| if (protocolId.includes('protocol.service.all')) { | ||
| return true; | ||
| } | ||
|
|
||
| // Check for specific service protocol | ||
| if (protocolId.includes(`protocol.service.${serviceName}`)) { | ||
| return true; | ||
| } | ||
|
|
||
| // Check for service category match | ||
| const serviceCategory = serviceName.split('.')[0]; | ||
| if (protocolId.includes(`protocol.service.${serviceCategory}`)) { | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Using .includes() for protocol ID matching is unsafe because it can match substrings anywhere in the string. For example, a capability with protocol ID 'com.malicious.protocol.service.all.bypass' would match the check for 'protocol.service.all' on line 263. Consider using exact string matching or proper prefix matching (e.g., startsWith, endsWith, or regex patterns) to prevent capability bypass attacks.
| function signPlugin(pluginCode: string, privateKeyPath: string): string { | ||
| const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); | ||
| const sign = crypto.createSign('SHA256'); | ||
| sign.update(pluginCode); | ||
| return sign.sign(privateKey, 'base64'); | ||
| } | ||
|
|
||
| // Sign your plugin | ||
| const pluginCode = fs.readFileSync('./my-plugin.js', 'utf8'); | ||
| const signature = signPlugin(pluginCode, './private-key.pem'); |
There was a problem hiding this comment.
The documentation shows signing the plugin file contents (line 96: fs.readFileSync('./my-plugin.js', 'utf8')), but the actual implementation serializes plugin functions using .toString() (plugin-signature-verifier.ts lines 264-281). This mismatch means signatures created following this guide will not verify correctly. The documentation should be updated to match the actual implementation, or preferably, the implementation should be fixed to sign actual file contents as documented.
| function signPlugin(pluginCode: string, privateKeyPath: string): string { | |
| const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); | |
| const sign = crypto.createSign('SHA256'); | |
| sign.update(pluginCode); | |
| return sign.sign(privateKey, 'base64'); | |
| } | |
| // Sign your plugin | |
| const pluginCode = fs.readFileSync('./my-plugin.js', 'utf8'); | |
| const signature = signPlugin(pluginCode, './private-key.pem'); | |
| // Serialize your plugin exactly the same way the verifier does | |
| // (the verifier uses function.toString() on plugin functions). | |
| function serializePluginForSigning(plugin: PluginMetadata): string { | |
| return plugin.init.toString(); | |
| } | |
| function signPlugin(serializedPlugin: string, privateKeyPath: string): string { | |
| const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); | |
| const sign = crypto.createSign('SHA256'); | |
| sign.update(serializedPlugin); | |
| return sign.sign(privateKey, 'base64'); | |
| } | |
| // Sign your plugin using the same serialization as the verifier | |
| const serializedPlugin = serializePluginForSigning(MyPlugin); | |
| const signature = signPlugin(serializedPlugin, './private-key.pem'); |
| export class PluginSignatureVerifier { | ||
| private config: PluginSignatureConfig; | ||
| private logger: Logger; | ||
|
|
||
| constructor(config: PluginSignatureConfig, logger: Logger) { | ||
| this.config = config; | ||
| this.logger = logger; | ||
|
|
||
| this.validateConfig(); | ||
| } | ||
|
|
||
| /** | ||
| * Verify plugin signature | ||
| * | ||
| * @param plugin - Plugin metadata with signature | ||
| * @returns Verification result | ||
| * @throws Error if verification fails in strict mode | ||
| */ | ||
| async verifyPluginSignature(plugin: PluginMetadata): Promise<SignatureVerificationResult> { | ||
| // Handle unsigned plugins | ||
| if (!plugin.signature) { | ||
| return this.handleUnsignedPlugin(plugin); | ||
| } | ||
|
|
||
| try { | ||
| // 1. Extract publisher ID from plugin name (reverse domain notation) | ||
| const publisherId = this.extractPublisherId(plugin.name); | ||
|
|
||
| // 2. Get trusted public key for publisher | ||
| const publicKey = this.config.trustedPublicKeys.get(publisherId); | ||
| if (!publicKey) { | ||
| const error = `No trusted public key for publisher: ${publisherId}`; | ||
| this.logger.warn(error, { plugin: plugin.name, publisherId }); | ||
|
|
||
| if (this.config.strictMode && !this.config.allowSelfSigned) { | ||
| throw new Error(error); | ||
| } | ||
|
|
||
| return { | ||
| verified: false, | ||
| error, | ||
| publisherId, | ||
| }; | ||
| } | ||
|
|
||
| // 3. Compute plugin code hash | ||
| const pluginHash = this.computePluginHash(plugin); | ||
|
|
||
| // 4. Verify signature using crypto module | ||
| const isValid = await this.verifyCryptoSignature( | ||
| pluginHash, | ||
| plugin.signature, | ||
| publicKey | ||
| ); | ||
|
|
||
| if (!isValid) { | ||
| const error = `Signature verification failed for plugin: ${plugin.name}`; | ||
| this.logger.error(error, { plugin: plugin.name, publisherId }); | ||
| throw new Error(error); | ||
| } | ||
|
|
||
| this.logger.info(`✅ Plugin signature verified: ${plugin.name}`, { | ||
| plugin: plugin.name, | ||
| publisherId, | ||
| algorithm: this.config.algorithm, | ||
| }); | ||
|
|
||
| return { | ||
| verified: true, | ||
| publisherId, | ||
| algorithm: this.config.algorithm, | ||
| }; | ||
|
|
||
| } catch (error) { | ||
| this.logger.error(`Signature verification error: ${plugin.name}`, error as Error); | ||
|
|
||
| if (this.config.strictMode) { | ||
| throw error; | ||
| } | ||
|
|
||
| return { | ||
| verified: false, | ||
| error: (error as Error).message, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register a trusted public key for a publisher | ||
| */ | ||
| registerPublicKey(publisherId: string, publicKey: string): void { | ||
| this.config.trustedPublicKeys.set(publisherId, publicKey); | ||
| this.logger.info(`Trusted public key registered for: ${publisherId}`); | ||
| } | ||
|
|
||
| /** | ||
| * Remove a trusted public key | ||
| */ | ||
| revokePublicKey(publisherId: string): void { | ||
| this.config.trustedPublicKeys.delete(publisherId); | ||
| this.logger.warn(`Public key revoked for: ${publisherId}`); | ||
| } | ||
|
|
||
| /** | ||
| * Get list of trusted publishers | ||
| */ | ||
| getTrustedPublishers(): string[] { | ||
| return Array.from(this.config.trustedPublicKeys.keys()); | ||
| } | ||
|
|
||
| // Private methods | ||
|
|
||
| private handleUnsignedPlugin(plugin: PluginMetadata): SignatureVerificationResult { | ||
| if (this.config.strictMode) { | ||
| const error = `Plugin missing signature (strict mode): ${plugin.name}`; | ||
| this.logger.error(error, { plugin: plugin.name }); | ||
| throw new Error(error); | ||
| } | ||
|
|
||
| this.logger.warn(`⚠️ Plugin not signed: ${plugin.name}`, { | ||
| plugin: plugin.name, | ||
| recommendation: 'Consider signing plugins for production environments', | ||
| }); | ||
|
|
||
| return { | ||
| verified: false, | ||
| error: 'Plugin not signed', | ||
| }; | ||
| } | ||
|
|
||
| private extractPublisherId(pluginName: string): string { | ||
| // Extract publisher from reverse domain notation | ||
| // Example: "com.objectstack.engine.objectql" -> "com.objectstack" | ||
| const parts = pluginName.split('.'); | ||
|
|
||
| if (parts.length < 2) { | ||
| throw new Error(`Invalid plugin name format: ${pluginName} (expected reverse domain notation)`); | ||
| } | ||
|
|
||
| // Return first two parts (domain reversed) | ||
| return `${parts[0]}.${parts[1]}`; | ||
| } | ||
|
|
||
| private computePluginHash(plugin: PluginMetadata): string { | ||
| // In browser environment, use SubtleCrypto | ||
| if (typeof window !== 'undefined') { | ||
| return this.computePluginHashBrowser(plugin); | ||
| } | ||
|
|
||
| // In Node.js environment, use crypto module | ||
| return this.computePluginHashNode(plugin); | ||
| } | ||
|
|
||
| private computePluginHashNode(plugin: PluginMetadata): string { | ||
| // Use pre-loaded crypto module | ||
| if (!cryptoModule) { | ||
| this.logger.warn('crypto module not available, using fallback hash'); | ||
| return this.computePluginHashFallback(plugin); | ||
| } | ||
|
|
||
| // Compute hash of plugin code | ||
| const pluginCode = this.serializePluginCode(plugin); | ||
| return cryptoModule.createHash('sha256').update(pluginCode).digest('hex'); | ||
| } | ||
|
|
||
| private computePluginHashBrowser(plugin: PluginMetadata): string { | ||
| // Browser environment - use simple hash for now | ||
| // In production, should use SubtleCrypto for proper cryptographic hash | ||
| this.logger.debug('Using browser hash (SubtleCrypto integration pending)'); | ||
| return this.computePluginHashFallback(plugin); | ||
| } | ||
|
|
||
| private computePluginHashFallback(plugin: PluginMetadata): string { | ||
| // Simple hash fallback (not cryptographically secure) | ||
| const pluginCode = this.serializePluginCode(plugin); | ||
| let hash = 0; | ||
|
|
||
| for (let i = 0; i < pluginCode.length; i++) { | ||
| const char = pluginCode.charCodeAt(i); | ||
| hash = ((hash << 5) - hash) + char; | ||
| hash = hash & hash; // Convert to 32-bit integer | ||
| } | ||
|
|
||
| return hash.toString(16); | ||
| } | ||
|
|
||
| private serializePluginCode(plugin: PluginMetadata): string { | ||
| // Serialize plugin code for hashing | ||
| // Include init, start, destroy functions | ||
| const parts: string[] = [ | ||
| plugin.name, | ||
| plugin.version, | ||
| plugin.init.toString(), | ||
| ]; | ||
|
|
||
| if (plugin.start) { | ||
| parts.push(plugin.start.toString()); | ||
| } | ||
|
|
||
| if (plugin.destroy) { | ||
| parts.push(plugin.destroy.toString()); | ||
| } | ||
|
|
||
| return parts.join('|'); | ||
| } | ||
|
|
||
| private async verifyCryptoSignature( | ||
| data: string, | ||
| signature: string, | ||
| publicKey: string | ||
| ): Promise<boolean> { | ||
| // In browser environment, use SubtleCrypto | ||
| if (typeof window !== 'undefined') { | ||
| return this.verifyCryptoSignatureBrowser(data, signature, publicKey); | ||
| } | ||
|
|
||
| // In Node.js environment, use crypto module | ||
| return this.verifyCryptoSignatureNode(data, signature, publicKey); | ||
| } | ||
|
|
||
| private verifyCryptoSignatureNode( | ||
| data: string, | ||
| signature: string, | ||
| publicKey: string | ||
| ): boolean { | ||
| if (!cryptoModule) { | ||
| this.logger.error('Crypto module not available for signature verification'); | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| // Create verify object based on algorithm | ||
| if (this.config.algorithm === 'ES256') { | ||
| // ECDSA verification - requires lowercase 'sha256' | ||
| const verify = cryptoModule.createVerify('sha256'); | ||
| verify.update(data); | ||
| return verify.verify( | ||
| { | ||
| key: publicKey, | ||
| format: 'pem', | ||
| type: 'spki', | ||
| }, | ||
| signature, | ||
| 'base64' | ||
| ); | ||
| } else { | ||
| // RSA verification (RS256) | ||
| const verify = cryptoModule.createVerify('RSA-SHA256'); | ||
| verify.update(data); | ||
| return verify.verify(publicKey, signature, 'base64'); | ||
| } | ||
| } catch (error) { | ||
| this.logger.error('Signature verification failed', error as Error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private async verifyCryptoSignatureBrowser( | ||
| _data: string, | ||
| _signature: string, | ||
| _publicKey: string | ||
| ): Promise<boolean> { | ||
| // Browser implementation using SubtleCrypto | ||
| // TODO: Implement SubtleCrypto-based verification | ||
| this.logger.warn('Browser signature verification not yet implemented'); | ||
| return false; | ||
| } | ||
|
|
||
| private validateConfig(): void { | ||
| if (!this.config.trustedPublicKeys || this.config.trustedPublicKeys.size === 0) { | ||
| this.logger.warn('No trusted public keys configured - all signatures will fail'); | ||
| } | ||
|
|
||
| if (!this.config.algorithm) { | ||
| throw new Error('Signature algorithm must be specified'); | ||
| } | ||
|
|
||
| if (!['RS256', 'ES256'].includes(this.config.algorithm)) { | ||
| throw new Error(`Unsupported algorithm: ${this.config.algorithm}`); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The PluginSignatureVerifier class has no test coverage, despite being a critical security component. Tests should verify signature verification success/failure cases, algorithm handling, strict mode behavior, publisher ID extraction, and error handling. This is essential for ensuring the security implementation works correctly.
- Rename PluginConfigValidator interface to IPluginConfigValidator to avoid conflict - Fix import paths in test files (security/ subfolder) - Fix logger.error calls to use correct signature (message, error?, meta?) - Fix globalThis.window checks using type assertions - Add missing 'certified: false' field to all PluginCapability test objects - Add type annotation for ZodIssue in formatZodErrors - Cast configSchema to any for partial() method call Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… silent to error log level - Fix ZodError.errors to ZodError.issues for Zod 4.x compatibility - Change test log level from 'silent' to 'error' (silent not supported) - Fix getService mock type signature with generic parameter Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Problem
ObjectStack kernel has comprehensive protocol schemas (109 Zod definitions) but lacks runtime enforcement. Plugin signature verification and config validation were TODO stubs. Permission enforcement existed only as capability schemas without runtime checks.
Assessment
MICROKERNEL_ASSESSMENT.md (Chinese) documents gap analysis:
Implementation
Security Module (
packages/core/src/security/)PluginSignatureVerifier
PluginConfigValidator
PluginPermissionEnforcer
SecurePluginContextwrapper for transparent runtime enforcementUsage
Defense in Depth
Files
Notes
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.