@onlineapps/service-validator-core 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -0
- package/coverage/clover.xml +468 -0
- package/coverage/coverage-final.json +8 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +146 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/index.js.html +643 -0
- package/coverage/lcov-report/src/security/certificateManager.js.html +799 -0
- package/coverage/lcov-report/src/security/index.html +131 -0
- package/coverage/lcov-report/src/security/tokenManager.js.html +622 -0
- package/coverage/lcov-report/src/validators/connectorValidator.js.html +787 -0
- package/coverage/lcov-report/src/validators/endpointValidator.js.html +577 -0
- package/coverage/lcov-report/src/validators/healthValidator.js.html +655 -0
- package/coverage/lcov-report/src/validators/index.html +161 -0
- package/coverage/lcov-report/src/validators/openApiValidator.js.html +517 -0
- package/coverage/lcov.info +982 -0
- package/jest.config.js +21 -0
- package/package.json +31 -0
- package/src/index.js +212 -0
- package/src/security/ValidationProofVerifier.js +178 -0
- package/src/security/certificateManager.js +239 -0
- package/src/security/tokenManager.js +194 -0
- package/src/validators/connectorValidator.js +235 -0
- package/src/validators/endpointValidator.js +165 -0
- package/src/validators/healthValidator.js +191 -0
- package/src/validators/openApiValidator.js +145 -0
- package/test/component/validation-flow.test.js +353 -0
- package/test/integration/real-validation.test.js +548 -0
- package/test/unit/ValidationCore.test.js +320 -0
package/jest.config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
coverageDirectory: 'coverage',
|
|
4
|
+
collectCoverageFrom: [
|
|
5
|
+
'src/**/*.js',
|
|
6
|
+
'!src/**/*.test.js',
|
|
7
|
+
'!src/**/*.spec.js'
|
|
8
|
+
],
|
|
9
|
+
testMatch: [
|
|
10
|
+
'<rootDir>/test/**/*.test.js'
|
|
11
|
+
],
|
|
12
|
+
coverageThreshold: {
|
|
13
|
+
global: {
|
|
14
|
+
branches: 80,
|
|
15
|
+
functions: 80,
|
|
16
|
+
lines: 80,
|
|
17
|
+
statements: 80
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
testTimeout: 10000
|
|
21
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onlineapps/service-validator-core",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Core validation logic for microservices",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"test:coverage": "jest --coverage",
|
|
9
|
+
"lint": "eslint src/"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"validation",
|
|
13
|
+
"microservices",
|
|
14
|
+
"openapi"
|
|
15
|
+
],
|
|
16
|
+
"author": "OnlineApps",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"ajv": "^8.12.0",
|
|
20
|
+
"jsonwebtoken": "^9.0.2",
|
|
21
|
+
"uuid": "^9.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"jest": "^29.7.0",
|
|
25
|
+
"eslint": "^8.56.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org/",
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const OpenApiValidator = require('./validators/openApiValidator');
|
|
2
|
+
const EndpointValidator = require('./validators/endpointValidator');
|
|
3
|
+
const ConnectorValidator = require('./validators/connectorValidator');
|
|
4
|
+
const HealthValidator = require('./validators/healthValidator');
|
|
5
|
+
const TokenManager = require('./security/tokenManager');
|
|
6
|
+
const CertificateManager = require('./security/certificateManager');
|
|
7
|
+
const ValidationProofVerifier = require('./security/ValidationProofVerifier');
|
|
8
|
+
|
|
9
|
+
class ValidationCore {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.config = {
|
|
12
|
+
strictMode: config.strictMode || false,
|
|
13
|
+
requiredConnectors: config.requiredConnectors || [
|
|
14
|
+
'connector-logger',
|
|
15
|
+
'connector-storage',
|
|
16
|
+
'connector-registry-client',
|
|
17
|
+
'connector-mq-client',
|
|
18
|
+
'connector-cookbook'
|
|
19
|
+
],
|
|
20
|
+
...config
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
this.validators = {
|
|
24
|
+
openApi: new OpenApiValidator(),
|
|
25
|
+
endpoints: new EndpointValidator(),
|
|
26
|
+
connectors: new ConnectorValidator(this.config.requiredConnectors),
|
|
27
|
+
health: new HealthValidator()
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this.tokenManager = new TokenManager();
|
|
31
|
+
this.certificateManager = new CertificateManager();
|
|
32
|
+
this.proofVerifier = new ValidationProofVerifier({
|
|
33
|
+
maxProofAge: config.maxProofAge,
|
|
34
|
+
strictMode: this.config.strictMode
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validates service configuration
|
|
40
|
+
* @param {object} serviceData - Service data to validate
|
|
41
|
+
* @returns {Promise<object>} Validation results
|
|
42
|
+
*/
|
|
43
|
+
async validate(serviceData) {
|
|
44
|
+
const results = {
|
|
45
|
+
success: true,
|
|
46
|
+
validated: false,
|
|
47
|
+
checks: {},
|
|
48
|
+
errors: [],
|
|
49
|
+
warnings: [],
|
|
50
|
+
timestamp: new Date().toISOString()
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Run all validators
|
|
55
|
+
if (serviceData.openApiSpec) {
|
|
56
|
+
const openApiResult = await this.validators.openApi.validate(serviceData.openApiSpec);
|
|
57
|
+
results.checks.openApi = openApiResult;
|
|
58
|
+
if (!openApiResult.valid) {
|
|
59
|
+
results.errors.push(...openApiResult.errors);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (serviceData.endpoints) {
|
|
64
|
+
const endpointsResult = await this.validators.endpoints.validate(serviceData.endpoints);
|
|
65
|
+
results.checks.endpoints = endpointsResult;
|
|
66
|
+
if (!endpointsResult.valid) {
|
|
67
|
+
results.errors.push(...endpointsResult.errors);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (serviceData.metadata) {
|
|
72
|
+
const connectorsResult = await this.validators.connectors.validate(serviceData.metadata);
|
|
73
|
+
results.checks.connectors = connectorsResult;
|
|
74
|
+
if (!connectorsResult.valid) {
|
|
75
|
+
results.warnings.push(...connectorsResult.warnings);
|
|
76
|
+
if (this.config.strictMode) {
|
|
77
|
+
results.errors.push(...connectorsResult.warnings);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (serviceData.health) {
|
|
83
|
+
const healthResult = await this.validators.health.validate(serviceData.health);
|
|
84
|
+
results.checks.health = healthResult;
|
|
85
|
+
if (!healthResult.valid) {
|
|
86
|
+
results.warnings.push(...healthResult.warnings);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
results.validated = true;
|
|
91
|
+
results.success = results.errors.length === 0;
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
results.success = false;
|
|
95
|
+
results.errors.push(`Validation error: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate pre-validation token
|
|
103
|
+
* @param {object} validationResults - Results from validate()
|
|
104
|
+
* @param {string} serviceName - Name of the service
|
|
105
|
+
* @returns {Promise<object>} Token data object with token, secret, and tokenId
|
|
106
|
+
*/
|
|
107
|
+
async generatePreValidationToken(validationResults, serviceName) {
|
|
108
|
+
if (!validationResults.success) {
|
|
109
|
+
throw new Error('Cannot generate token for failed validation');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tokenData = await this.tokenManager.generateToken({
|
|
113
|
+
serviceName,
|
|
114
|
+
validationResults,
|
|
115
|
+
type: 'pre-validation'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Store secret for later verification (in real implementation, store in secure storage)
|
|
119
|
+
this.tokenSecrets = this.tokenSecrets || new Map();
|
|
120
|
+
this.tokenSecrets.set(tokenData.token, tokenData.secret);
|
|
121
|
+
|
|
122
|
+
// Return full token data object (token, secret, tokenId)
|
|
123
|
+
return tokenData;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Verify pre-validation token
|
|
128
|
+
* @param {string} token - Token to verify
|
|
129
|
+
* @param {string} secret - Secret to verify with (optional)
|
|
130
|
+
* @returns {Promise<object>} Token payload
|
|
131
|
+
*/
|
|
132
|
+
async verifyPreValidationToken(token, secret = null) {
|
|
133
|
+
// Use provided secret or lookup from stored secrets
|
|
134
|
+
const verifySecret = secret || (this.tokenSecrets && this.tokenSecrets.get(token)) || null;
|
|
135
|
+
return this.tokenManager.verifyToken(token, verifySecret);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generate validation certificate
|
|
140
|
+
* @param {object} validationResults - Results from validate()
|
|
141
|
+
* @param {string} serviceName - Name of the service
|
|
142
|
+
* @param {string} version - Service version
|
|
143
|
+
* @returns {Promise<object>} Validation certificate
|
|
144
|
+
*/
|
|
145
|
+
async generateCertificate(validationResults, serviceName, version) {
|
|
146
|
+
if (!validationResults.success) {
|
|
147
|
+
throw new Error('Cannot generate certificate for failed validation');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.certificateManager.generateCertificate({
|
|
151
|
+
serviceName,
|
|
152
|
+
version,
|
|
153
|
+
validationResults,
|
|
154
|
+
issuedAt: new Date().toISOString(),
|
|
155
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() // 30 days
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Verify validation certificate
|
|
161
|
+
* @param {object} certificate - Certificate to verify
|
|
162
|
+
* @returns {Promise<boolean>} Verification result
|
|
163
|
+
*/
|
|
164
|
+
async verifyCertificate(certificate) {
|
|
165
|
+
return this.certificateManager.verifyCertificate(certificate);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Verify validation proof (SHA256-based from pre-validation)
|
|
170
|
+
* @param {object} proof - Complete proof object from .validation-proof.json
|
|
171
|
+
* @returns {object} Verification result with {valid, reason, details}
|
|
172
|
+
*/
|
|
173
|
+
verifyValidationProof(proof) {
|
|
174
|
+
return this.proofVerifier.verifyProof(proof);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Generate functional tests from OpenAPI spec
|
|
179
|
+
* @param {object} openApiSpec - OpenAPI specification
|
|
180
|
+
* @returns {Promise<array>} Generated tests
|
|
181
|
+
*/
|
|
182
|
+
async generateFunctionalTests(openApiSpec) {
|
|
183
|
+
const tests = [];
|
|
184
|
+
|
|
185
|
+
if (!openApiSpec || !openApiSpec.paths) {
|
|
186
|
+
return tests;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const [pathName, pathDef] of Object.entries(openApiSpec.paths)) {
|
|
190
|
+
for (const [method, operation] of Object.entries(pathDef)) {
|
|
191
|
+
if (['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
|
|
192
|
+
tests.push({
|
|
193
|
+
name: operation.summary || `${method.toUpperCase()} ${pathName}`,
|
|
194
|
+
method: method.toUpperCase(),
|
|
195
|
+
path: pathName,
|
|
196
|
+
description: operation.description || '',
|
|
197
|
+
parameters: operation.parameters || [],
|
|
198
|
+
requestBody: operation.requestBody || null,
|
|
199
|
+
expectedResponses: Object.keys(operation.responses || {}),
|
|
200
|
+
expectedSuccessResponse: operation.responses?.['200'] ? '200' :
|
|
201
|
+
operation.responses?.['201'] ? '201' : null
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return tests;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = ValidationCore;
|
|
212
|
+
module.exports.ValidationProofVerifier = ValidationProofVerifier;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ValidationProofVerifier - Verifies SHA256 validation proofs from pre-validation
|
|
5
|
+
*
|
|
6
|
+
* This class verifies validation proofs generated by ValidationProofGenerator
|
|
7
|
+
* from the conn-e2e-testing package. It validates:
|
|
8
|
+
* - Hash integrity (SHA256)
|
|
9
|
+
* - Proof structure and format
|
|
10
|
+
* - Test results (passed/failed counts)
|
|
11
|
+
* - Proof age (max 7 days)
|
|
12
|
+
* - Validator identity
|
|
13
|
+
*/
|
|
14
|
+
class ValidationProofVerifier {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.maxProofAge = options.maxProofAge || 7 * 24 * 60 * 60 * 1000; // 7 days default
|
|
17
|
+
this.strictMode = options.strictMode || false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify validation proof
|
|
22
|
+
* @param {object} proof - Complete proof object from .validation-proof.json
|
|
23
|
+
* @returns {object} Verification result with {valid, reason, details}
|
|
24
|
+
*/
|
|
25
|
+
verifyProof(proof) {
|
|
26
|
+
console.log('[ValidationProofVerifier] Starting proof verification');
|
|
27
|
+
console.log('[ValidationProofVerifier] Proof hash:', proof.validationProof?.substring(0, 16) + '...');
|
|
28
|
+
|
|
29
|
+
// 1. Check proof structure
|
|
30
|
+
if (!proof || typeof proof !== 'object') {
|
|
31
|
+
console.error('[ValidationProofVerifier] ❌ Invalid proof structure: not an object');
|
|
32
|
+
return { valid: false, reason: 'INVALID_STRUCTURE', details: 'Proof must be an object' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!proof.validationProof || typeof proof.validationProof !== 'string') {
|
|
36
|
+
console.error('[ValidationProofVerifier] ❌ Missing or invalid validationProof field');
|
|
37
|
+
return { valid: false, reason: 'MISSING_HASH', details: 'validationProof field is required' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!proof.validationData || typeof proof.validationData !== 'object') {
|
|
41
|
+
console.error('[ValidationProofVerifier] ❌ Missing or invalid validationData field');
|
|
42
|
+
return { valid: false, reason: 'MISSING_DATA', details: 'validationData field is required' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Check hash format (SHA256 = 64 hex characters)
|
|
46
|
+
if (!/^[a-f0-9]{64}$/i.test(proof.validationProof)) {
|
|
47
|
+
console.error('[ValidationProofVerifier] ❌ Invalid hash format:', proof.validationProof);
|
|
48
|
+
return { valid: false, reason: 'INVALID_HASH_FORMAT', details: 'Hash must be 64-character hex string (SHA256)' };
|
|
49
|
+
}
|
|
50
|
+
console.log('[ValidationProofVerifier] ✓ Hash format valid (SHA256)');
|
|
51
|
+
|
|
52
|
+
// 3. Verify hash integrity
|
|
53
|
+
const data = proof.validationData;
|
|
54
|
+
const recomputedHash = this._generateHash(data);
|
|
55
|
+
|
|
56
|
+
if (recomputedHash !== proof.validationProof) {
|
|
57
|
+
console.error('[ValidationProofVerifier] ❌ Hash mismatch');
|
|
58
|
+
console.error('[ValidationProofVerifier] Expected:', proof.validationProof);
|
|
59
|
+
console.error('[ValidationProofVerifier] Computed:', recomputedHash);
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
reason: 'HASH_MISMATCH',
|
|
63
|
+
details: 'Validation data does not match hash (data may have been tampered with)'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
console.log('[ValidationProofVerifier] ✓ Hash integrity verified');
|
|
67
|
+
|
|
68
|
+
// 4. Check required validationData fields
|
|
69
|
+
const requiredFields = ['serviceName', 'version', 'testsRun', 'testsPassed', 'testsFailed', 'validator', 'timestamp'];
|
|
70
|
+
for (const field of requiredFields) {
|
|
71
|
+
if (data[field] === undefined || data[field] === null) {
|
|
72
|
+
console.error(`[ValidationProofVerifier] ❌ Missing required field: ${field}`);
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
reason: 'MISSING_FIELD',
|
|
76
|
+
details: `Required field missing: ${field}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
console.log('[ValidationProofVerifier] ✓ All required fields present');
|
|
81
|
+
|
|
82
|
+
// 5. Check proof age
|
|
83
|
+
const proofDate = new Date(data.timestamp);
|
|
84
|
+
const now = new Date();
|
|
85
|
+
const age = now - proofDate;
|
|
86
|
+
|
|
87
|
+
if (isNaN(proofDate.getTime())) {
|
|
88
|
+
console.error('[ValidationProofVerifier] ❌ Invalid timestamp:', data.timestamp);
|
|
89
|
+
return { valid: false, reason: 'INVALID_TIMESTAMP', details: 'Timestamp is not a valid date' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (age > this.maxProofAge) {
|
|
93
|
+
const maxDays = Math.floor(this.maxProofAge / (24 * 60 * 60 * 1000));
|
|
94
|
+
const ageDays = Math.floor(age / (24 * 60 * 60 * 1000));
|
|
95
|
+
console.error(`[ValidationProofVerifier] ❌ Proof too old: ${ageDays} days (max ${maxDays} days)`);
|
|
96
|
+
return {
|
|
97
|
+
valid: false,
|
|
98
|
+
reason: 'PROOF_EXPIRED',
|
|
99
|
+
details: `Proof is ${ageDays} days old (max ${maxDays} days)`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
console.log(`[ValidationProofVerifier] ✓ Proof age valid: ${Math.floor(age / (60 * 60 * 1000))} hours old`);
|
|
103
|
+
|
|
104
|
+
// 6. Check test results
|
|
105
|
+
if (data.testsRun <= 0) {
|
|
106
|
+
console.error('[ValidationProofVerifier] ❌ No tests were run');
|
|
107
|
+
return { valid: false, reason: 'NO_TESTS', details: 'At least one test must be run' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (data.testsFailed > 0) {
|
|
111
|
+
console.error(`[ValidationProofVerifier] ❌ Some tests failed: ${data.testsFailed}/${data.testsRun}`);
|
|
112
|
+
return {
|
|
113
|
+
valid: false,
|
|
114
|
+
reason: 'TESTS_FAILED',
|
|
115
|
+
details: `${data.testsFailed} out of ${data.testsRun} tests failed`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (data.testsPassed !== data.testsRun) {
|
|
120
|
+
console.error('[ValidationProofVerifier] ❌ Test count mismatch');
|
|
121
|
+
return {
|
|
122
|
+
valid: false,
|
|
123
|
+
reason: 'TEST_COUNT_MISMATCH',
|
|
124
|
+
details: `Passed (${data.testsPassed}) + Failed (${data.testsFailed}) ≠ Total (${data.testsRun})`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
console.log(`[ValidationProofVerifier] ✓ All tests passed: ${data.testsPassed}/${data.testsRun}`);
|
|
128
|
+
|
|
129
|
+
// 7. Check validator identity
|
|
130
|
+
if (!data.validator || typeof data.validator !== 'string') {
|
|
131
|
+
console.error('[ValidationProofVerifier] ❌ Invalid validator identity');
|
|
132
|
+
return { valid: false, reason: 'INVALID_VALIDATOR', details: 'Validator identity is required' };
|
|
133
|
+
}
|
|
134
|
+
console.log('[ValidationProofVerifier] ✓ Validator:', data.validator);
|
|
135
|
+
|
|
136
|
+
// 8. All checks passed
|
|
137
|
+
console.log('[ValidationProofVerifier] ✅ Proof verification successful');
|
|
138
|
+
console.log(`[ValidationProofVerifier] Service: ${data.serviceName} v${data.version}`);
|
|
139
|
+
console.log(`[ValidationProofVerifier] Tests: ${data.testsPassed}/${data.testsRun} passed`);
|
|
140
|
+
console.log(`[ValidationProofVerifier] Validator: ${data.validator}`);
|
|
141
|
+
console.log(`[ValidationProofVerifier] Timestamp: ${data.timestamp}`);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
valid: true,
|
|
145
|
+
reason: 'VALID',
|
|
146
|
+
details: {
|
|
147
|
+
serviceName: data.serviceName,
|
|
148
|
+
version: data.version,
|
|
149
|
+
testsRun: data.testsRun,
|
|
150
|
+
testsPassed: data.testsPassed,
|
|
151
|
+
validator: data.validator,
|
|
152
|
+
timestamp: data.timestamp,
|
|
153
|
+
fingerprint: data.fingerprint
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate SHA256 hash from validation data (must match ValidationProofGenerator)
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
_generateHash(data) {
|
|
163
|
+
const hashInput = JSON.stringify({
|
|
164
|
+
serviceName: data.serviceName,
|
|
165
|
+
version: data.version,
|
|
166
|
+
testsRun: data.testsRun,
|
|
167
|
+
testsPassed: data.testsPassed,
|
|
168
|
+
testsFailed: data.testsFailed,
|
|
169
|
+
validator: data.validator,
|
|
170
|
+
timestamp: data.timestamp,
|
|
171
|
+
fingerprint: data.fingerprint || ''
|
|
172
|
+
}, null, 0); // No whitespace for consistency
|
|
173
|
+
|
|
174
|
+
return crypto.createHash('sha256').update(hashInput).digest('hex');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = ValidationProofVerifier;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class CertificateManager {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.algorithm = config.algorithm || 'RSA-SHA256';
|
|
6
|
+
this.keyPair = config.keyPair || this.generateKeyPair();
|
|
7
|
+
this.issuer = config.issuer || 'validation-service';
|
|
8
|
+
this.validityDays = config.validityDays || 30;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate RSA key pair for signing certificates
|
|
13
|
+
* @returns {object} Key pair
|
|
14
|
+
*/
|
|
15
|
+
generateKeyPair() {
|
|
16
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
|
|
17
|
+
modulusLength: 2048,
|
|
18
|
+
publicKeyEncoding: {
|
|
19
|
+
type: 'spki',
|
|
20
|
+
format: 'pem'
|
|
21
|
+
},
|
|
22
|
+
privateKeyEncoding: {
|
|
23
|
+
type: 'pkcs8',
|
|
24
|
+
format: 'pem'
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return { publicKey, privateKey };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a validation certificate
|
|
33
|
+
* @param {object} data - Certificate data
|
|
34
|
+
* @returns {Promise<object>} Generated certificate
|
|
35
|
+
*/
|
|
36
|
+
async generateCertificate(data) {
|
|
37
|
+
const certificateId = crypto.randomBytes(16).toString('hex');
|
|
38
|
+
const issuedAt = new Date();
|
|
39
|
+
const expiresAt = new Date(issuedAt);
|
|
40
|
+
expiresAt.setDate(expiresAt.getDate() + this.validityDays);
|
|
41
|
+
|
|
42
|
+
const certificate = {
|
|
43
|
+
id: certificateId,
|
|
44
|
+
version: '1.0',
|
|
45
|
+
issuer: this.issuer,
|
|
46
|
+
subject: {
|
|
47
|
+
serviceName: data.serviceName,
|
|
48
|
+
serviceVersion: data.version
|
|
49
|
+
},
|
|
50
|
+
issuedAt: issuedAt.toISOString(),
|
|
51
|
+
expiresAt: expiresAt.toISOString(),
|
|
52
|
+
validationResults: {
|
|
53
|
+
timestamp: data.validationResults.timestamp,
|
|
54
|
+
success: data.validationResults.success,
|
|
55
|
+
checks: data.validationResults.checks
|
|
56
|
+
},
|
|
57
|
+
fingerprint: this.generateFingerprint(data.validationResults),
|
|
58
|
+
extensions: {
|
|
59
|
+
allowedOperations: data.allowedOperations || ['register', 'heartbeat', 'workflow'],
|
|
60
|
+
restrictions: data.restrictions || [],
|
|
61
|
+
metadata: data.metadata || {}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Sign the certificate
|
|
66
|
+
certificate.signature = this.signData(certificate);
|
|
67
|
+
|
|
68
|
+
return certificate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Verify a validation certificate
|
|
73
|
+
* @param {object} certificate - Certificate to verify
|
|
74
|
+
* @returns {Promise<boolean>} Verification result
|
|
75
|
+
*/
|
|
76
|
+
async verifyCertificate(certificate) {
|
|
77
|
+
try {
|
|
78
|
+
// Check expiration
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const expiresAt = new Date(certificate.expiresAt);
|
|
81
|
+
if (now > expiresAt) {
|
|
82
|
+
throw new Error('Certificate has expired');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check issued date (not in future)
|
|
86
|
+
const issuedAt = new Date(certificate.issuedAt);
|
|
87
|
+
if (now < issuedAt) {
|
|
88
|
+
throw new Error('Certificate issued in the future');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Verify signature
|
|
92
|
+
const certCopy = { ...certificate };
|
|
93
|
+
const providedSignature = certCopy.signature;
|
|
94
|
+
delete certCopy.signature;
|
|
95
|
+
|
|
96
|
+
const calculatedSignature = this.signData(certCopy);
|
|
97
|
+
if (providedSignature !== calculatedSignature) {
|
|
98
|
+
throw new Error('Invalid certificate signature');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Verify fingerprint
|
|
102
|
+
const calculatedFingerprint = this.generateFingerprint(certificate.validationResults);
|
|
103
|
+
if (certificate.fingerprint !== calculatedFingerprint) {
|
|
104
|
+
throw new Error('Certificate data has been tampered with');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check issuer
|
|
108
|
+
if (certificate.issuer !== this.issuer) {
|
|
109
|
+
throw new Error(`Unknown certificate issuer: ${certificate.issuer}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Certificate verification failed:', error.message);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Sign data with private key
|
|
121
|
+
* @param {object} data - Data to sign
|
|
122
|
+
* @returns {string} Signature
|
|
123
|
+
*/
|
|
124
|
+
signData(data) {
|
|
125
|
+
const sign = crypto.createSign(this.algorithm);
|
|
126
|
+
sign.update(JSON.stringify(data, Object.keys(data).sort()));
|
|
127
|
+
sign.end();
|
|
128
|
+
return sign.sign(this.keyPair.privateKey, 'hex');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Verify data signature with public key
|
|
133
|
+
* @param {object} data - Data to verify
|
|
134
|
+
* @param {string} signature - Signature to verify
|
|
135
|
+
* @returns {boolean} Verification result
|
|
136
|
+
*/
|
|
137
|
+
verifySignature(data, signature) {
|
|
138
|
+
const verify = crypto.createVerify(this.algorithm);
|
|
139
|
+
verify.update(JSON.stringify(data, Object.keys(data).sort()));
|
|
140
|
+
verify.end();
|
|
141
|
+
return verify.verify(this.keyPair.publicKey, signature, 'hex');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate fingerprint for data
|
|
146
|
+
* @param {object} data - Data to fingerprint
|
|
147
|
+
* @returns {string} Fingerprint
|
|
148
|
+
*/
|
|
149
|
+
generateFingerprint(data) {
|
|
150
|
+
const sortedData = JSON.stringify(data, Object.keys(data).sort());
|
|
151
|
+
return crypto.createHash('sha256').update(sortedData).digest('hex');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Revoke a certificate (would need persistent storage in production)
|
|
156
|
+
* @param {string} certificateId - Certificate ID to revoke
|
|
157
|
+
* @returns {Promise<void>}
|
|
158
|
+
*/
|
|
159
|
+
async revokeCertificate(certificateId) {
|
|
160
|
+
// In production, this would update a revocation list in database
|
|
161
|
+
// For now, we'll just log it
|
|
162
|
+
console.log(`Certificate ${certificateId} has been revoked`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if a certificate is revoked
|
|
167
|
+
* @param {string} certificateId - Certificate ID to check
|
|
168
|
+
* @returns {Promise<boolean>} True if revoked
|
|
169
|
+
*/
|
|
170
|
+
async isCertificateRevoked(certificateId) {
|
|
171
|
+
// In production, this would check against a revocation list
|
|
172
|
+
// For now, always return false
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Export public key for distribution
|
|
178
|
+
* @returns {string} Public key in PEM format
|
|
179
|
+
*/
|
|
180
|
+
exportPublicKey() {
|
|
181
|
+
return this.keyPair.publicKey;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generate a certificate chain (for future use with intermediate CAs)
|
|
186
|
+
* @param {object} certificate - Leaf certificate
|
|
187
|
+
* @param {Array} chain - Certificate chain
|
|
188
|
+
* @returns {object} Complete chain
|
|
189
|
+
*/
|
|
190
|
+
generateCertificateChain(certificate, chain = []) {
|
|
191
|
+
return {
|
|
192
|
+
certificate,
|
|
193
|
+
chain,
|
|
194
|
+
rootCA: this.exportPublicKey()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate certificate permissions
|
|
200
|
+
* @param {object} certificate - Certificate to check
|
|
201
|
+
* @param {string} operation - Operation to validate
|
|
202
|
+
* @returns {boolean} True if operation is allowed
|
|
203
|
+
*/
|
|
204
|
+
validatePermissions(certificate, operation) {
|
|
205
|
+
if (!certificate.extensions || !certificate.extensions.allowedOperations) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return certificate.extensions.allowedOperations.includes(operation);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate a validation report from certificate
|
|
214
|
+
* @param {object} certificate - Certificate to analyze
|
|
215
|
+
* @returns {object} Validation report
|
|
216
|
+
*/
|
|
217
|
+
generateValidationReport(certificate) {
|
|
218
|
+
const now = new Date();
|
|
219
|
+
const expiresAt = new Date(certificate.expiresAt);
|
|
220
|
+
const daysRemaining = Math.floor((expiresAt - now) / (1000 * 60 * 60 * 24));
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
certificateId: certificate.id,
|
|
224
|
+
serviceName: certificate.subject.serviceName,
|
|
225
|
+
serviceVersion: certificate.subject.serviceVersion,
|
|
226
|
+
issuer: certificate.issuer,
|
|
227
|
+
issuedAt: certificate.issuedAt,
|
|
228
|
+
expiresAt: certificate.expiresAt,
|
|
229
|
+
daysRemaining,
|
|
230
|
+
isExpired: daysRemaining < 0,
|
|
231
|
+
validationPassed: certificate.validationResults.success,
|
|
232
|
+
checksPerformed: Object.keys(certificate.validationResults.checks || {}),
|
|
233
|
+
allowedOperations: certificate.extensions.allowedOperations,
|
|
234
|
+
restrictions: certificate.extensions.restrictions
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = CertificateManager;
|