mirror of
https://github.com/selfxyz/self.git
synced 2026-01-12 16:18:19 -05:00
* chore: centralize license header scripts * chore: run license header checks from root * add header to other files * add header to bundle * add migration script and update check license headers * convert license to mobile sdk * migrate license headers * remove headers from common; convert remaining * fix headers * add license header checks
460 lines
18 KiB
TypeScript
460 lines
18 KiB
TypeScript
import { Buffer } from 'buffer';
|
|
import { ec } from 'elliptic';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import cose from '../src/utils/cose.js';
|
|
|
|
import { decode, encode } from '@stablelib/cbor';
|
|
|
|
// Set up mocks before importing the module
|
|
vi.mock('@stablelib/cbor', () => ({
|
|
decode: vi.fn(),
|
|
encode: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('elliptic', () => ({
|
|
ec: vi.fn(),
|
|
}));
|
|
|
|
describe('cose.sign.verify', () => {
|
|
let mockVerifyFn: any;
|
|
let mockKeyFromPublicFn: any;
|
|
|
|
beforeEach(() => {
|
|
// Reset mocks before each test
|
|
vi.mocked(decode).mockClear();
|
|
vi.mocked(encode).mockClear();
|
|
vi.mocked(ec).mockClear();
|
|
|
|
// Create fresh mock functions for each test
|
|
mockVerifyFn = vi.fn();
|
|
mockKeyFromPublicFn = vi.fn().mockReturnValue({
|
|
verify: mockVerifyFn,
|
|
});
|
|
|
|
// Set up the EC constructor mock
|
|
vi.mocked(ec).mockImplementation(function EC(curve: string) {
|
|
// Validate supported curves for edge case testing
|
|
if (!['p256', 'p384'].includes(curve)) {
|
|
throw new Error(`Unsupported curve: ${curve}`);
|
|
}
|
|
|
|
this.keyFromPublic = mockKeyFromPublicFn;
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
// Realistic test data with valid cryptographic values
|
|
const validP256Verifier = {
|
|
key: {
|
|
x: '1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83',
|
|
y: 'ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9',
|
|
curve: 'p256',
|
|
},
|
|
};
|
|
|
|
const validP384Verifier = {
|
|
key: {
|
|
x: '1fbac8eebd0cbf35640b39efe0808dd774debff20a2a329e91713baf7d7f3c3e81546d883730bee7e48678f857b02ca0',
|
|
y: 'eb213103bd68ce343365a8a4c3d4555fa385f5330203bdd76ffad1f3affb95751c132007e1b240353cb0a4cf1693bdf9',
|
|
curve: 'p384',
|
|
},
|
|
};
|
|
|
|
const malformedVerifier = {
|
|
key: {
|
|
x: 'invalid_hex',
|
|
y: '1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83',
|
|
curve: 'p256',
|
|
},
|
|
};
|
|
|
|
const wrongLengthVerifier = {
|
|
key: {
|
|
x: '1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf', // Too short
|
|
y: 'ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9',
|
|
curve: 'p256',
|
|
},
|
|
};
|
|
|
|
const unsupportedCurveVerifier = {
|
|
key: {
|
|
x: '1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83',
|
|
y: 'ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9',
|
|
curve: 'secp256k1', // Unsupported curve
|
|
},
|
|
};
|
|
|
|
// Realistic protected header with algorithm identifier
|
|
const protectedHeaderBytes = new Uint8Array([0xa1, 0x01, 0x26]); // {1: -7} for ES256
|
|
const p384ProtectedHeaderBytes = new Uint8Array([0xa1, 0x01, 0x38, 0x22]); // {1: -35} for ES384
|
|
const payload = Buffer.from('test payload data');
|
|
const validSignature = Buffer.from(Array.from({ length: 64 }, (_, i) => i % 256)); // 64 bytes for p256
|
|
const validP384Signature = Buffer.from(Array.from({ length: 96 }, (_, i) => i % 256)); // 96 bytes for p384
|
|
const oddLengthSignature = Buffer.from(Array.from({ length: 63 }, (_, i) => i % 256)); // Invalid odd length
|
|
|
|
const validCoseData = Buffer.from([1, 2, 3, 4]); // Sample COSE data
|
|
|
|
describe('valid signature verification', () => {
|
|
it('accepts valid p256 signature', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
const expectedSigStructure = ['Signature1', protectedHeaderBytes, new Uint8Array(0), payload];
|
|
const encodedSigStructure = new Uint8Array([1, 2, 3]);
|
|
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(encodedSigStructure);
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 });
|
|
|
|
// Verify CBOR functions called with correct parameters
|
|
expect(vi.mocked(decode)).toHaveBeenCalledWith(new Uint8Array(validCoseData));
|
|
expect(vi.mocked(encode)).toHaveBeenCalledWith(expectedSigStructure);
|
|
});
|
|
|
|
it('accepts valid p384 signature', async () => {
|
|
const expectedDecoded = [p384ProtectedHeaderBytes, {}, payload, validP384Signature];
|
|
const expectedSigStructure = [
|
|
'Signature1',
|
|
p384ProtectedHeaderBytes,
|
|
new Uint8Array(0),
|
|
payload,
|
|
];
|
|
const encodedSigStructure = new Uint8Array([1, 2, 3]);
|
|
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(encodedSigStructure);
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
await cose.sign.verify(validCoseData, validP384Verifier, { defaultType: 35 });
|
|
|
|
// Verify CBOR functions called with correct parameters
|
|
expect(vi.mocked(decode)).toHaveBeenCalledWith(new Uint8Array(validCoseData));
|
|
expect(vi.mocked(encode)).toHaveBeenCalledWith(expectedSigStructure);
|
|
});
|
|
});
|
|
|
|
describe('invalid signature verification', () => {
|
|
it('rejects invalid signature', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
|
|
it('handles exceptions during key creation from public key', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
mockKeyFromPublicFn.mockImplementation(() => {
|
|
throw new Error('Invalid public key format');
|
|
});
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid public key format');
|
|
});
|
|
|
|
it('handles exceptions during signature verification', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
mockVerifyFn.mockImplementation(() => {
|
|
throw new Error('Signature verification computation failed');
|
|
});
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Signature verification computation failed');
|
|
});
|
|
|
|
it('handles malformed signature r/s components', async () => {
|
|
// Create a signature with invalid r/s split (all zeros in r part)
|
|
const malformedSignature = Buffer.concat([
|
|
Buffer.alloc(32, 0), // All zeros for r component
|
|
Buffer.from(Array.from({ length: 32 }, (_, i) => i % 256)), // Valid s component
|
|
]);
|
|
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, malformedSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
|
|
it('handles exceptions during elliptic curve initialization', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
// Use a verifier with an unsupported curve to trigger the EC constructor error
|
|
const invalidCurveVerifier = {
|
|
key: {
|
|
x: '1ccbe91c075fc7f4f033bfa248db8fccd3565de94bbfb12f3c59ff46c271bf83',
|
|
y: 'ce4014c68811f9a21a1fdb2c0e6113e06db7ca93b7404e78dc7ccd5ca89a4ca9',
|
|
curve: 'secp256k1', // This will trigger the "Unsupported curve" error from our mock
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, invalidCurveVerifier, {
|
|
defaultType: 18,
|
|
})
|
|
).rejects.toThrow('Unsupported curve: secp256k1');
|
|
});
|
|
|
|
it('handles verification returning unexpected non-boolean values', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(undefined);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
|
|
it('handles corrupted signature data causing buffer operations to fail', async () => {
|
|
// Create a signature that would cause issues during r/s extraction
|
|
const corruptedSignature = Buffer.from([0xff, 0xfe]); // Too short, but even length
|
|
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, corruptedSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
|
|
it('handles CBOR encode failure during signature structure creation', async () => {
|
|
const expectedDecoded = [protectedHeaderBytes, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockImplementation(() => {
|
|
throw new Error('CBOR encoding failed');
|
|
});
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('CBOR encoding failed');
|
|
});
|
|
|
|
it('handles signature verification with tampered payload', async () => {
|
|
const tamperedPayload = Buffer.from('tampered payload data');
|
|
const expectedDecoded = [
|
|
protectedHeaderBytes,
|
|
{},
|
|
tamperedPayload, // Different from original payload
|
|
validSignature,
|
|
];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
|
|
it('handles verification with corrupted protected header', async () => {
|
|
const corruptedProtectedHeader = new Uint8Array([0x80, 0x81, 0x82]); // Invalid CBOR
|
|
const expectedDecoded = [corruptedProtectedHeader, {}, payload, validSignature];
|
|
vi.mocked(decode).mockReturnValue(expectedDecoded);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
});
|
|
|
|
describe('COSE format edge cases', () => {
|
|
it('throws error when decode returns non-array', async () => {
|
|
vi.mocked(decode).mockReturnValue('not an array');
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid COSE_Sign1 format');
|
|
});
|
|
|
|
it('throws error when decode returns array with wrong length', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload]); // Missing signature
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid COSE_Sign1 format');
|
|
});
|
|
|
|
it('throws error when decode returns empty array', async () => {
|
|
vi.mocked(decode).mockReturnValue([]);
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid COSE_Sign1 format');
|
|
});
|
|
});
|
|
|
|
describe('signature format edge cases', () => {
|
|
it('throws error for odd-length signature buffer', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, oddLengthSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid signature length');
|
|
});
|
|
|
|
it('handles empty signature buffer reaching verification', async () => {
|
|
const emptySignature = Buffer.alloc(0);
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, emptySignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(false);
|
|
|
|
// Empty signature (length 0) passes length check since 0 % 2 === 0
|
|
// but creates empty r/s hex strings that fail verification
|
|
await expect(
|
|
cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 })
|
|
).rejects.toThrow('AWS root certificate signature verification failed');
|
|
});
|
|
});
|
|
|
|
describe('cryptographic key edge cases', () => {
|
|
it('throws error for malformed key coordinates', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
// Configure keyFromPublic to throw error for malformed coordinates
|
|
mockKeyFromPublicFn.mockImplementation((publicKey: { x: string; y: string }) => {
|
|
if (!/^[0-9a-fA-F]+$/.test(publicKey.x) || !/^[0-9a-fA-F]+$/.test(publicKey.y)) {
|
|
throw new Error('Invalid key coordinates: must be hexadecimal');
|
|
}
|
|
return { verify: mockVerifyFn };
|
|
});
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, malformedVerifier, { defaultType: 18 })
|
|
).rejects.toThrow('Invalid key coordinates: must be hexadecimal');
|
|
});
|
|
|
|
it('throws error for wrong coordinate length', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
// Configure keyFromPublic to throw error for wrong length
|
|
mockKeyFromPublicFn.mockImplementation((publicKey: { x: string; y: string }) => {
|
|
const expectedLength = 64; // p256
|
|
if (publicKey.x.length !== expectedLength || publicKey.y.length !== expectedLength) {
|
|
throw new Error(`Invalid key coordinate length for curve p256`);
|
|
}
|
|
return { verify: mockVerifyFn };
|
|
});
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, wrongLengthVerifier, {
|
|
defaultType: 18,
|
|
})
|
|
).rejects.toThrow('Invalid key coordinate length for curve p256');
|
|
});
|
|
|
|
it('throws error for unsupported curve', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
|
|
await expect(
|
|
cose.sign.verify(validCoseData, unsupportedCurveVerifier, {
|
|
defaultType: 18,
|
|
})
|
|
).rejects.toThrow('Unsupported curve: secp256k1');
|
|
});
|
|
});
|
|
|
|
describe('defaultType option behavior', () => {
|
|
it('accepts different defaultType values without affecting verification', async () => {
|
|
vi.mocked(decode).mockReturnValue([protectedHeaderBytes, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
// Test different defaultType values (currently not used in implementation)
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 });
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 35 });
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: -7 });
|
|
|
|
// All should succeed without throwing
|
|
expect(vi.mocked(decode)).toHaveBeenCalledTimes(3);
|
|
expect(vi.mocked(encode)).toHaveBeenCalledTimes(3);
|
|
expect(vi.mocked(ec)).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
describe('protected header edge cases', () => {
|
|
it('handles empty protected header', async () => {
|
|
const emptyProtectedHeader = new Uint8Array([0xa0]); // Empty CBOR map
|
|
vi.mocked(decode).mockReturnValue([emptyProtectedHeader, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 });
|
|
|
|
// Should succeed without throwing
|
|
expect(vi.mocked(decode)).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(encode)).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(ec)).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('handles malformed protected header bytes', async () => {
|
|
const malformedProtectedHeader = new Uint8Array([0xff, 0xff]); // Invalid CBOR
|
|
vi.mocked(decode).mockReturnValue([malformedProtectedHeader, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
// Note: Currently the implementation doesn't validate protected header content
|
|
// This test documents current behavior and prepares for future validation
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 });
|
|
|
|
// Should succeed without throwing
|
|
expect(vi.mocked(decode)).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(encode)).toHaveBeenCalledTimes(1);
|
|
expect(vi.mocked(ec)).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('handles various protected header structures', async () => {
|
|
const complexProtectedHeader = new Uint8Array([
|
|
0xa2,
|
|
0x01,
|
|
0x26,
|
|
0x04,
|
|
0x42,
|
|
0x31,
|
|
0x32, // {1: -7, 4: "12"}
|
|
]);
|
|
vi.mocked(decode).mockReturnValue([complexProtectedHeader, {}, payload, validSignature]);
|
|
vi.mocked(encode).mockReturnValue(new Uint8Array([1, 2, 3]));
|
|
mockVerifyFn.mockReturnValue(true);
|
|
|
|
await cose.sign.verify(validCoseData, validP256Verifier, { defaultType: 18 });
|
|
|
|
// Verify the protected header is passed correctly to the signature structure
|
|
expect(vi.mocked(encode)).toHaveBeenCalledWith([
|
|
'Signature1',
|
|
complexProtectedHeader,
|
|
new Uint8Array(0),
|
|
payload,
|
|
]);
|
|
});
|
|
});
|
|
});
|