-
Notifications
You must be signed in to change notification settings - Fork 911
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Validate that the public key generated from createKeyPairFromBytes() …
…belongs to the private key (#2329) This PR addresses #2289 updated `createKeyPairFromBytes`. Adds a validation check to verify the public and private key by signing data and verifying that the signed data matches the public key. Adds error code for public/private key mismatches. Included in this PR is testcases to test the possible errors within these functions and creating a successful keypair from a 64-byte array.
Alex
authored
Mar 26, 2024
1 parent
9370133
commit 478443f
Showing
8 changed files
with
161 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@solana/assertions": patch | ||
"@solana/errors": patch | ||
"@solana/keys": patch | ||
--- | ||
|
||
`createKeyPairFromBytes()` now validates that the public key imported is the one that would be derived from the private key imported |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; | ||
|
||
import { assertPRNGIsAvailable } from '../crypto'; | ||
|
||
describe('assertPRNGIsAvailable()', () => { | ||
describe('when getRandomValues is available', () => { | ||
it('does not throw', () => { | ||
expect(assertPRNGIsAvailable).not.toThrow(); | ||
}); | ||
it('returns `undefined`', () => { | ||
expect(assertPRNGIsAvailable()).toBeUndefined(); | ||
}); | ||
}); | ||
describe('when getRandomValues is not available', () => { | ||
let oldCrypto: InstanceType<typeof Crypto>['getRandomValues']; | ||
beforeEach(() => { | ||
oldCrypto = globalThis.crypto.getRandomValues; | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
globalThis.crypto.getRandomValues = undefined; | ||
}); | ||
afterEach(() => { | ||
globalThis.crypto.getRandomValues = oldCrypto; | ||
}); | ||
it('throws', () => { | ||
expect(assertPRNGIsAvailable).toThrow( | ||
new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED), | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; | ||
|
||
export function assertPRNGIsAvailable() { | ||
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.getRandomValues !== 'function') { | ||
throw new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from './subtle-crypto'; | ||
|
||
export * from './crypto'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,35 +1,88 @@ | ||
import { generateKeyPair } from '../key-pair'; | ||
import { | ||
SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, | ||
SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, | ||
SolanaError, | ||
} from '@solana/errors'; | ||
|
||
describe('generateKeyPair', () => { | ||
it.each(['private', 'public'])('generates an ed25519 %s `CryptoKey`', async type => { | ||
expect.assertions(1); | ||
const keyPair = await generateKeyPair(); | ||
expect(keyPair).toMatchObject({ | ||
[`${type}Key`]: expect.objectContaining({ | ||
[Symbol.toStringTag]: 'CryptoKey', | ||
algorithm: { name: 'Ed25519' }, | ||
type, | ||
}), | ||
import { createKeyPairFromBytes, generateKeyPair } from '../key-pair'; | ||
|
||
const MOCK_KEY_BYTES = new Uint8Array([ | ||
0xeb, 0xfa, 0x65, 0xeb, 0x93, 0xdc, 0x79, 0x15, 0x7a, 0xba, 0xde, 0xa2, 0xf7, 0x94, 0x37, 0x9d, 0xfc, 0x07, 0x1d, | ||
0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 0x1d, 0x0e, 0x93, 0x86, 0x4d, 0xcc, | ||
0x81, 0x5f, 0xc3, 0xf2, 0x86, 0x18, 0x09, 0x11, 0xd0, 0x0a, 0x3f, 0xd2, 0x06, 0xde, 0x31, 0xa1, 0xc9, 0x42, 0x87, | ||
0xcb, 0x43, 0xf0, 0x5f, 0xc9, 0xf2, 0xb5, | ||
]); | ||
|
||
const MOCK_INVALID_KEY_BYTES = new Uint8Array([ | ||
0xeb, 0xfa, 0x65, 0xeb, 0x93, 0xdc, 0x79, 0x15, 0x7a, 0xba, 0xde, 0xa2, 0xf7, 0x94, 0x37, 0x9d, 0xfc, 0x07, 0x1d, | ||
0x68, 0x86, 0x87, 0x37, 0x6d, 0xc5, 0xd5, 0xa0, 0x54, 0x12, 0x1d, 0x34, 0x4a, 0x1d, 0x0e, 0x93, 0x86, 0x4d, 0xcc, | ||
0x81, 0x5f, 0xc3, 0xf2, 0x86, 0x18, 0x09, 0x11, 0xd0, 0x0a, 0x3f, 0xd2, 0x06, 0xde, 0x31, 0xa1, 0xc9, 0x42, 0x87, | ||
0xcb, 0x43, 0xf0, 0x5f, 0xc9, 0xf2, 0xb1, | ||
]); | ||
|
||
describe('key-pair', () => { | ||
describe('generateKeyPair', () => { | ||
it.each(['private', 'public'])('generates an ed25519 %s `CryptoKey`', async type => { | ||
expect.assertions(1); | ||
const keyPair = await generateKeyPair(); | ||
expect(keyPair).toMatchObject({ | ||
[`${type}Key`]: expect.objectContaining({ | ||
[Symbol.toStringTag]: 'CryptoKey', | ||
algorithm: { name: 'Ed25519' }, | ||
type, | ||
}), | ||
}); | ||
}); | ||
it('generates a non-extractable private key', async () => { | ||
expect.assertions(1); | ||
const { privateKey } = await generateKeyPair(); | ||
expect(privateKey).toHaveProperty('extractable', false); | ||
}); | ||
it('generates a private key usable for signing operations', async () => { | ||
expect.assertions(1); | ||
const { privateKey } = await generateKeyPair(); | ||
expect(privateKey).toHaveProperty('usages', ['sign']); | ||
}); | ||
it('generates an extractable public key', async () => { | ||
expect.assertions(1); | ||
const { publicKey } = await generateKeyPair(); | ||
expect(publicKey).toHaveProperty('extractable', true); | ||
}); | ||
it('generates a public key usable for verifying signatures', async () => { | ||
expect.assertions(1); | ||
const { publicKey } = await generateKeyPair(); | ||
expect(publicKey).toHaveProperty('usages', ['verify']); | ||
}); | ||
}); | ||
it('generates a non-extractable private key', async () => { | ||
expect.assertions(1); | ||
const { privateKey } = await generateKeyPair(); | ||
expect(privateKey).toHaveProperty('extractable', false); | ||
}); | ||
it('generates a private key usable for signing operations', async () => { | ||
expect.assertions(1); | ||
const { privateKey } = await generateKeyPair(); | ||
expect(privateKey).toHaveProperty('usages', ['sign']); | ||
}); | ||
it('generates an extractable public key', async () => { | ||
expect.assertions(1); | ||
const { publicKey } = await generateKeyPair(); | ||
expect(publicKey).toHaveProperty('extractable', true); | ||
}); | ||
it('generates a public key usable for verifying signatures', async () => { | ||
expect.assertions(1); | ||
const { publicKey } = await generateKeyPair(); | ||
expect(publicKey).toHaveProperty('usages', ['verify']); | ||
|
||
describe('createKeyPairFromBytes', () => { | ||
it('creates a key pair from a 64-byte array', async () => { | ||
expect.assertions(1); | ||
const keyPair = await createKeyPairFromBytes(MOCK_KEY_BYTES); | ||
expect(keyPair).toMatchObject({ | ||
privateKey: expect.objectContaining({ | ||
[Symbol.toStringTag]: 'CryptoKey', | ||
algorithm: { name: 'Ed25519' }, | ||
type: 'private', | ||
}), | ||
publicKey: expect.objectContaining({ | ||
[Symbol.toStringTag]: 'CryptoKey', | ||
algorithm: { name: 'Ed25519' }, | ||
type: 'public', | ||
}), | ||
}); | ||
}); | ||
it('errors when the byte array is not 64 bytes', async () => { | ||
expect.assertions(1); | ||
await expect(createKeyPairFromBytes(MOCK_KEY_BYTES.slice(0, 31))).rejects.toThrow( | ||
new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: 31 }), | ||
); | ||
}); | ||
it('errors when public key fails signature verification', async () => { | ||
expect.assertions(1); | ||
await expect(createKeyPairFromBytes(MOCK_INVALID_KEY_BYTES)).rejects.toThrow( | ||
new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY), | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters