344 lines
8.5 KiB
TypeScript
344 lines
8.5 KiB
TypeScript
/**
|
|
* Unit tests for CryptID WebCrypto utilities
|
|
*
|
|
* Tests the core cryptographic functions:
|
|
* - Key pair generation
|
|
* - Public key export
|
|
* - Challenge signing
|
|
* - Signature verification
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
|
|
// We'll test the crypto utilities directly
|
|
// First, let's verify our mock setup works
|
|
|
|
describe('WebCrypto Environment', () => {
|
|
it('crypto.subtle is available', () => {
|
|
expect(crypto).toBeDefined()
|
|
expect(crypto.subtle).toBeDefined()
|
|
})
|
|
|
|
it('crypto.getRandomValues works', () => {
|
|
const array = new Uint8Array(16)
|
|
crypto.getRandomValues(array)
|
|
|
|
// Should have random values (not all zeros)
|
|
const hasNonZero = array.some(v => v !== 0)
|
|
expect(hasNonZero).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('ECDSA Key Generation', () => {
|
|
it('can generate ECDSA P-256 key pair', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true, // extractable
|
|
['sign', 'verify']
|
|
)
|
|
|
|
expect(keyPair).toBeDefined()
|
|
expect(keyPair.privateKey).toBeDefined()
|
|
expect(keyPair.publicKey).toBeDefined()
|
|
})
|
|
|
|
it('generated keys have correct algorithm', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
expect(keyPair.privateKey.algorithm.name).toBe('ECDSA')
|
|
expect(keyPair.publicKey.algorithm.name).toBe('ECDSA')
|
|
})
|
|
|
|
it('private key is extractable when requested', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true, // extractable = true
|
|
['sign', 'verify']
|
|
)
|
|
|
|
expect(keyPair.privateKey.extractable).toBe(true)
|
|
expect(keyPair.publicKey.extractable).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Public Key Export', () => {
|
|
it('can export public key in raw format', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const exportedKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
|
|
|
// jsdom may return ArrayBuffer or ArrayBuffer-like object
|
|
expect(exportedKey).toBeDefined()
|
|
expect(exportedKey.byteLength).toBeDefined()
|
|
// P-256 raw public key is 65 bytes (uncompressed point)
|
|
expect(exportedKey.byteLength).toBe(65)
|
|
})
|
|
|
|
it('can export public key in spki format', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const exportedKey = await crypto.subtle.exportKey('spki', keyPair.publicKey)
|
|
|
|
// jsdom may return ArrayBuffer or ArrayBuffer-like object
|
|
expect(exportedKey).toBeDefined()
|
|
expect(exportedKey.byteLength).toBeDefined()
|
|
expect(exportedKey.byteLength).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('exported key can be converted to base64', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const exportedKey = await crypto.subtle.exportKey('raw', keyPair.publicKey)
|
|
const base64Key = btoa(String.fromCharCode(...new Uint8Array(exportedKey)))
|
|
|
|
expect(typeof base64Key).toBe('string')
|
|
expect(base64Key.length).toBeGreaterThan(0)
|
|
// Base64 should be roughly 4/3 * 65 bytes ≈ 88 chars
|
|
expect(base64Key.length).toBeGreaterThan(80)
|
|
})
|
|
})
|
|
|
|
describe('Challenge Signing', () => {
|
|
it('can sign a challenge string', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const challenge = 'testuser:1234567890:randomchallenge'
|
|
const encoder = new TextEncoder()
|
|
const data = encoder.encode(challenge)
|
|
|
|
const signature = await crypto.subtle.sign(
|
|
{
|
|
name: 'ECDSA',
|
|
hash: 'SHA-256',
|
|
},
|
|
keyPair.privateKey,
|
|
data
|
|
)
|
|
|
|
// jsdom may return ArrayBuffer or ArrayBuffer-like object
|
|
expect(signature).toBeDefined()
|
|
expect(signature.byteLength).toBeDefined()
|
|
expect(signature.byteLength).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('different challenges produce different signatures', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const encoder = new TextEncoder()
|
|
|
|
const sig1 = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.privateKey,
|
|
encoder.encode('challenge1')
|
|
)
|
|
|
|
const sig2 = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.privateKey,
|
|
encoder.encode('challenge2')
|
|
)
|
|
|
|
// Signatures should be different
|
|
const sig1Arr = new Uint8Array(sig1)
|
|
const sig2Arr = new Uint8Array(sig2)
|
|
|
|
let isDifferent = false
|
|
for (let i = 0; i < Math.min(sig1Arr.length, sig2Arr.length); i++) {
|
|
if (sig1Arr[i] !== sig2Arr[i]) {
|
|
isDifferent = true
|
|
break
|
|
}
|
|
}
|
|
|
|
expect(isDifferent).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Signature Verification', () => {
|
|
it('verifies valid signature', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const encoder = new TextEncoder()
|
|
const data = encoder.encode('test challenge')
|
|
|
|
const signature = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.privateKey,
|
|
data
|
|
)
|
|
|
|
const isValid = await crypto.subtle.verify(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.publicKey,
|
|
signature,
|
|
data
|
|
)
|
|
|
|
expect(isValid).toBe(true)
|
|
})
|
|
|
|
it('rejects tampered data', async () => {
|
|
const keyPair = await crypto.subtle.generateKey(
|
|
{
|
|
name: 'ECDSA',
|
|
namedCurve: 'P-256',
|
|
},
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const encoder = new TextEncoder()
|
|
const originalData = encoder.encode('original challenge')
|
|
const tamperedData = encoder.encode('tampered challenge')
|
|
|
|
const signature = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.privateKey,
|
|
originalData
|
|
)
|
|
|
|
// Verify with tampered data should fail
|
|
const isValid = await crypto.subtle.verify(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair.publicKey,
|
|
signature,
|
|
tamperedData
|
|
)
|
|
|
|
expect(isValid).toBe(false)
|
|
})
|
|
|
|
it('rejects signature from different key', async () => {
|
|
const keyPair1 = await crypto.subtle.generateKey(
|
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const keyPair2 = await crypto.subtle.generateKey(
|
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
const encoder = new TextEncoder()
|
|
const data = encoder.encode('test challenge')
|
|
|
|
// Sign with key pair 1
|
|
const signature = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair1.privateKey,
|
|
data
|
|
)
|
|
|
|
// Verify with key pair 2's public key should fail
|
|
const isValid = await crypto.subtle.verify(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
keyPair2.publicKey, // Different key!
|
|
signature,
|
|
data
|
|
)
|
|
|
|
expect(isValid).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Key Import/Export Round Trip', () => {
|
|
it('can export and re-import public key', async () => {
|
|
const originalKeyPair = await crypto.subtle.generateKey(
|
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
true,
|
|
['sign', 'verify']
|
|
)
|
|
|
|
// Export public key
|
|
const exported = await crypto.subtle.exportKey('raw', originalKeyPair.publicKey)
|
|
|
|
// Re-import
|
|
const importedKey = await crypto.subtle.importKey(
|
|
'raw',
|
|
exported,
|
|
{ name: 'ECDSA', namedCurve: 'P-256' },
|
|
true,
|
|
['verify']
|
|
)
|
|
|
|
expect(importedKey).toBeDefined()
|
|
expect(importedKey.algorithm.name).toBe('ECDSA')
|
|
|
|
// Verify a signature with the imported key
|
|
const encoder = new TextEncoder()
|
|
const data = encoder.encode('test data')
|
|
|
|
const signature = await crypto.subtle.sign(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
originalKeyPair.privateKey,
|
|
data
|
|
)
|
|
|
|
const isValid = await crypto.subtle.verify(
|
|
{ name: 'ECDSA', hash: 'SHA-256' },
|
|
importedKey,
|
|
signature,
|
|
data
|
|
)
|
|
|
|
expect(isValid).toBe(true)
|
|
})
|
|
})
|