Files
claude-hub/test/unit/security/signature-verification.test.js
Jonathan Flatt 3a56ee0499 feat: improve test coverage for TypeScript files
- Add comprehensive tests for index.ts (91.93% coverage)
- Add tests for routes/claude.ts (91.66% coverage)
- Add tests for routes/github.ts (100% coverage)
- Add tests for utils/startup-metrics.ts (100% coverage)
- Add tests for utils/sanitize.ts with actual exported functions
- Add tests for routes/chatbot.js
- Update test configuration to exclude test files from TypeScript build
- Fix linting issues in test files
- Install @types/supertest for TypeScript test support
- Update .gitignore to exclude compiled TypeScript test artifacts

Overall test coverage improved from ~65% to 76.5%

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-28 15:49:30 +00:00

432 lines
13 KiB
JavaScript

const crypto = require('crypto');
const DiscordProvider = require('../../../src/providers/DiscordProvider');
// Mock dependencies
jest.mock('../../../src/utils/logger', () => ({
createLogger: () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn()
})
}));
jest.mock('../../../src/utils/secureCredentials', () => ({
get: jest.fn()
}));
const mockSecureCredentials = require('../../../src/utils/secureCredentials');
describe.skip('Signature Verification Security Tests', () => {
let provider;
const validPublicKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const _validPrivateKey = 'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789';
// Helper function to run test with production NODE_ENV
const withProductionEnv = testFn => {
const originalNodeEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
try {
return testFn();
} finally {
process.env.NODE_ENV = originalNodeEnv;
}
};
beforeEach(() => {
mockSecureCredentials.get.mockImplementation(key => {
const mockCreds = {
DISCORD_BOT_TOKEN: 'mock_bot_token',
DISCORD_PUBLIC_KEY: validPublicKey,
DISCORD_APPLICATION_ID: '123456789012345678'
};
return mockCreds[key];
});
provider = new DiscordProvider();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Discord Ed25519 Signature Verification', () => {
beforeEach(async () => {
await provider.initialize();
});
it('should reject requests with missing signature headers', () => {
const req = {
headers: {},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should reject requests with only timestamp header', () => {
const req = {
headers: {
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should reject requests with only signature header', () => {
const req = {
headers: {
'x-signature-ed25519': 'some_signature'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle invalid signature format gracefully', () => {
withProductionEnv(() => {
const req = {
headers: {
'x-signature-ed25519': 'invalid_hex_signature',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
// Should not throw an error, but return false
expect(() => provider.verifyWebhookSignature(req)).not.toThrow();
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
});
it('should handle invalid public key format gracefully', async () => {
// Override with invalid key format
mockSecureCredentials.get.mockImplementation(key => {
if (key === 'DISCORD_PUBLIC_KEY') return 'invalid_key_format';
return 'mock_value';
});
const invalidProvider = new DiscordProvider();
await invalidProvider.initialize();
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(invalidProvider.verifyWebhookSignature(req)).toBe(false);
});
it('should bypass verification in test mode', () => {
const originalEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'test';
const req = {
headers: {
'x-signature-ed25519': 'completely_invalid_signature',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(true);
process.env.NODE_ENV = originalEnv;
});
it('should handle crypto verification errors without throwing', () => {
// Mock crypto.verify to throw an error
const originalVerify = crypto.verify;
crypto.verify = jest.fn().mockImplementation(() => {
throw new Error('Crypto verification failed');
});
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(() => provider.verifyWebhookSignature(req)).not.toThrow();
expect(provider.verifyWebhookSignature(req)).toBe(false);
// Restore original function
crypto.verify = originalVerify;
});
it('should construct verification message correctly', () => {
const timestamp = '1234567890';
const body = 'test body content';
const expectedMessage = timestamp + body;
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': timestamp
},
rawBody: Buffer.from(body),
body: { test: 'data' }
};
// Mock crypto.verify to capture the message parameter
const originalVerify = crypto.verify;
const mockVerify = jest.fn().mockReturnValue(false);
crypto.verify = mockVerify;
provider.verifyWebhookSignature(req);
expect(mockVerify).toHaveBeenCalledWith(
'ed25519',
Buffer.from(expectedMessage),
expect.any(Buffer), // public key buffer
expect.any(Buffer) // signature buffer
);
crypto.verify = originalVerify;
});
it('should use rawBody when available', () => {
const timestamp = '1234567890';
const rawBodyContent = 'raw body content';
const bodyContent = { parsed: 'json' };
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': timestamp
},
rawBody: Buffer.from(rawBodyContent),
body: bodyContent
};
const originalVerify = crypto.verify;
const mockVerify = jest.fn().mockReturnValue(false);
crypto.verify = mockVerify;
provider.verifyWebhookSignature(req);
// Should use rawBody, not JSON.stringify(body)
expect(mockVerify).toHaveBeenCalledWith(
'ed25519',
Buffer.from(timestamp + rawBodyContent),
expect.any(Buffer),
expect.any(Buffer)
);
crypto.verify = originalVerify;
});
it('should fallback to JSON.stringify when rawBody is unavailable', () => {
const timestamp = '1234567890';
const bodyContent = { test: 'data' };
const expectedMessage = timestamp + JSON.stringify(bodyContent);
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': timestamp
},
// No rawBody provided
body: bodyContent
};
const originalVerify = crypto.verify;
const mockVerify = jest.fn().mockReturnValue(false);
crypto.verify = mockVerify;
provider.verifyWebhookSignature(req);
expect(mockVerify).toHaveBeenCalledWith(
'ed25519',
Buffer.from(expectedMessage),
expect.any(Buffer),
expect.any(Buffer)
);
crypto.verify = originalVerify;
});
});
describe('Security Edge Cases', () => {
beforeEach(async () => {
await provider.initialize();
});
it('should handle empty signature gracefully', () => {
const req = {
headers: {
'x-signature-ed25519': '',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle empty timestamp gracefully', () => {
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': ''
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle signature with wrong length', () => {
const req = {
headers: {
'x-signature-ed25519': 'short_sig',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle very long signature without crashing', () => {
const req = {
headers: {
'x-signature-ed25519': 'a'.repeat(1000), // Very long signature
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(() => provider.verifyWebhookSignature(req)).not.toThrow();
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle unicode characters in timestamp', () => {
const req = {
headers: {
'x-signature-ed25519':
'64byte_hex_signature_placeholder_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'x-signature-timestamp': '123😀567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(() => provider.verifyWebhookSignature(req)).not.toThrow();
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle null/undefined headers safely', () => {
const req = {
headers: {
'x-signature-ed25519': null,
'x-signature-timestamp': undefined
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(provider.verifyWebhookSignature(req)).toBe(false);
});
it('should handle Buffer conversion errors gracefully', () => {
// Mock Buffer.from to throw an error
const originalBufferFrom = Buffer.from;
Buffer.from = jest.fn().mockImplementation(data => {
if (typeof data === 'string' && data.includes('signature')) {
throw new Error('Buffer conversion failed');
}
return originalBufferFrom(data);
});
const req = {
headers: {
'x-signature-ed25519': 'invalid_signature_that_causes_buffer_error',
'x-signature-timestamp': '1234567890'
},
rawBody: Buffer.from('test body'),
body: { test: 'data' }
};
expect(() => provider.verifyWebhookSignature(req)).not.toThrow();
expect(provider.verifyWebhookSignature(req)).toBe(false);
Buffer.from = originalBufferFrom;
});
});
describe('Timing Attack Prevention', () => {
beforeEach(async () => {
await provider.initialize();
});
it('should have consistent timing for different signature lengths', async () => {
const shortSig = 'abc';
const longSig = 'a'.repeat(128);
const timestamp = '1234567890';
const req1 = {
headers: {
'x-signature-ed25519': shortSig,
'x-signature-timestamp': timestamp
},
rawBody: Buffer.from('test'),
body: {}
};
const req2 = {
headers: {
'x-signature-ed25519': longSig,
'x-signature-timestamp': timestamp
},
rawBody: Buffer.from('test'),
body: {}
};
// Both should return false, and ideally take similar time
const start1 = process.hrtime.bigint();
const result1 = provider.verifyWebhookSignature(req1);
const end1 = process.hrtime.bigint();
const start2 = process.hrtime.bigint();
const result2 = provider.verifyWebhookSignature(req2);
const end2 = process.hrtime.bigint();
expect(result1).toBe(false);
expect(result2).toBe(false);
// Both operations should complete in reasonable time (less than 100ms)
const time1 = Number(end1 - start1) / 1000000; // Convert to milliseconds
const time2 = Number(end2 - start2) / 1000000;
expect(time1).toBeLessThan(100);
expect(time2).toBeLessThan(100);
});
});
});