-
Notifications
You must be signed in to change notification settings - Fork 916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[experimental] Validate that the public key generated from createKeyPairFromBytes() belongs to the private key #2329
Conversation
🦋 Changeset detectedLatest commit: 6e50c49 The changes in this PR will be included in the next version bump. This PR includes changesets to release 35 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Thank you for your contribution! I just added a few comments. 🙂 |
Thanks for the quick feedback! Your comments make sense, changes has been applied. lemme know if theres anything else |
packages/keys/src/key-pair.ts
Outdated
@@ -29,21 +29,8 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo | |||
const signedData = await signBytes(privateKey, TEST_DATA); | |||
const isValid = await verifySignature(publicKey, signedData, TEST_DATA); | |||
if (!isValid) { | |||
throw new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE); | |||
throw new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: bytes.slice(32) }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update! Just one more comment. It's probably more useful for the end-user if that address is base-58 encoded.
You can use string codecs to transform it like so:
import { getStringDecoder, getBase58Decoder } from "@solana/codecs-strings";
const addressDecoder = getStringDecoder({ encoding: getBase58Decoder(), size: 32 });
const address = addressDecoder.decode(bytes.slice(32));
Note that you could also use getAddressDecoder
from @solana/addresses
but that would add a new dependency to @solana/keys
when it's not really needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think we should probably not log the bytes here. Imagine that someone got the order wrong, and now their private bytes are logged to the console. Include no context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a very good point.
Ah also, don't forget to fix the formatting. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/keys/src/key-pair.ts
Outdated
|
||
import { createPrivateKeyFromBytes } from './private-key'; | ||
import { verifySignature, signBytes } from './signatures'; | ||
|
||
const TEST_DATA = new Uint8Array([1, 2, 3, 4, 5]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like this to be randomly generated on each pass through the function.
packages/keys/src/key-pair.ts
Outdated
const signedData = await signBytes(privateKey, TEST_DATA); | ||
const isValid = await verifySignature(publicKey, signedData, TEST_DATA); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const signedData = await signBytes(privateKey, TEST_DATA); | |
const isValid = await verifySignature(publicKey, signedData, TEST_DATA); | |
const randomBytes = new Uint8Array(32); | |
crypto.getRandomValues(randomBytes); | |
const signedData = await signBytes(privateKey, randomBytes); | |
const isValid = await verifySignature(publicKey, signedData, randomBytes); |
|
||
import { createPrivateKeyFromBytes } from './private-key'; | ||
import { verifySignature, signBytes } from './signatures'; | ||
|
||
const TEST_DATA = new Uint8Array([1, 2, 3, 4, 5]); | ||
|
||
export async function generateKeyPair(): Promise<CryptoKeyPair> { | ||
await assertKeyGenerationIsAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To generate a test message randomly each time, we'll need to assert that the PRNG is available (that crypto.getRandomValues
is a function. Can you add that, and a related error code, to
@solana/assertions`.
await assertKeyGenerationIsAvailable(); | |
await Promise.all([ | |
assertKeyGenerationIsAvailable(), | |
assertPRNGIsAvailable(), // <-- add this to `@solana/assertions` & `@solana/errors` | |
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make more sense to put assertPRNGIsAvailable()
in createKeyPairFromBytes
than generateKeyPair
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, of course. Oops; I left my comment in the wrong spot!
packages/keys/src/key-pair.ts
Outdated
@@ -29,21 +29,8 @@ export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: bo | |||
const signedData = await signBytes(privateKey, TEST_DATA); | |||
const isValid = await verifySignature(publicKey, signedData, TEST_DATA); | |||
if (!isValid) { | |||
throw new SolanaError(SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE); | |||
throw new SolanaError(SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY, { address: bytes.slice(32) }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think we should probably not log the bytes here. Imagine that someone got the order wrong, and now their private bytes are logged to the console. Include no context.
packages/errors/src/messages.ts
Outdated
@@ -402,6 +403,7 @@ export const SolanaErrorMessages: Readonly<{ | |||
'Expected base58-encoded signature to decode to a byte array of length 64. Actual length: $actualLength.', | |||
[SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE]: | |||
'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.', | |||
[SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key does not match the provided public key at address [$address].', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the wrong error code, right?
[SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE]: 'The provided private key does not match the provided public key at address [$address].', | |
[SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY]: 'The provided private key does not match the provided public key at address [$address].', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, thanks!
Thanks for doing this @luu-alex! |
|
||
import { createPrivateKeyFromBytes } from './private-key'; | ||
import { verifySignature, signBytes } from './signatures'; | ||
|
||
const TEST_DATA = new Uint8Array([1, 2, 3, 4, 5]); | ||
|
||
export async function generateKeyPair(): Promise<CryptoKeyPair> { | ||
await assertKeyGenerationIsAvailable(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would make more sense to put assertPRNGIsAvailable()
in createKeyPairFromBytes
than generateKeyPair
right?
Thanks for the feedback! 👍 let me know if theres anything else :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have the rights to push to your master
to update this PR, so here's the final boss level before merge!
diff --git a/packages/assertions/src/__tests__/crypto-test.ts b/packages/assertions/src/__tests__/crypto-test.ts
index a676917c4..5357cd790 100644
--- a/packages/assertions/src/__tests__/crypto-test.ts
+++ b/packages/assertions/src/__tests__/crypto-test.ts
@@ -3,9 +3,13 @@ import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError
import { assertPRNGIsAvailable } from '../crypto';
describe('assertPRNGIsAvailable()', () => {
- it('resolves to `undefined` without throwing', async () => {
- expect.assertions(1);
- await expect(assertPRNGIsAvailable()).resolves.toBeUndefined();
+ 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'];
@@ -18,9 +22,8 @@ describe('assertPRNGIsAvailable()', () => {
afterEach(() => {
globalThis.crypto.getRandomValues = oldCrypto;
});
- it('rejects', async () => {
- expect.assertions(1);
- await expect(() => assertPRNGIsAvailable()).rejects.toThrow(
+ it('throws', () => {
+ expect(assertPRNGIsAvailable).toThrow(
new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED),
);
});
diff --git a/packages/assertions/src/crypto.ts b/packages/assertions/src/crypto.ts
index bd62651c5..8752465e8 100644
--- a/packages/assertions/src/crypto.ts
+++ b/packages/assertions/src/crypto.ts
@@ -1,6 +1,6 @@
import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors';
-export async function assertPRNGIsAvailable() {
+export function assertPRNGIsAvailable() {
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.getRandomValues !== 'function') {
throw new SolanaError(SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED);
}
diff --git a/packages/assertions/src/index.ts b/packages/assertions/src/index.ts
index b3383c7a9..4d4f2dd31 100644
--- a/packages/assertions/src/index.ts
+++ b/packages/assertions/src/index.ts
@@ -1 +1,2 @@
+export * from './crypto';
export * from './subtle-crypto';
diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts
index feaac895e..5bac1bfea 100644
--- a/packages/errors/src/codes.ts
+++ b/packages/errors/src/codes.ts
@@ -92,7 +92,7 @@ export const SOLANA_ERROR__SUBTLE_CRYPTO__VERIFY_FUNCTION_UNIMPLEMENTED = 361000
// Crypto-related errors.
// Reserve error codes in the range [3611000-3611050].
-export const SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED = 3610007 as const;
+export const SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED = 3611000 as const;
// Key-related errors.
// Reserve error codes in the range [3704000-3704999].
diff --git a/packages/keys/src/key-pair.ts b/packages/keys/src/key-pair.ts
index 4adc36823..32c3264e8 100644
--- a/packages/keys/src/key-pair.ts
+++ b/packages/keys/src/key-pair.ts
@@ -1,4 +1,4 @@
-import { assertKeyGenerationIsAvailable } from '@solana/assertions';
+import { assertKeyGenerationIsAvailable, assertPRNGIsAvailable } from '@solana/assertions';
import {
SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH,
SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY,
@@ -19,6 +19,8 @@ export async function generateKeyPair(): Promise<CryptoKeyPair> {
}
export async function createKeyPairFromBytes(bytes: Uint8Array, extractable?: boolean): Promise<CryptoKeyPair> {
+ assertPRNGIsAvailable();
+
if (bytes.byteLength !== 64) {
throw new SolanaError(SOLANA_ERROR__KEYS__INVALID_KEY_PAIR_BYTE_LENGTH, { byteLength: bytes.byteLength });
}
packages/assertions/src/crypto.ts
Outdated
@@ -0,0 +1,7 @@ | |||
import { SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, SolanaError } from '@solana/errors'; | |||
|
|||
export async function assertPRNGIsAvailable() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm… I wonder why some of these were made async
. Seems like a mistake we shouldn't replicate here.
Cool. feedback addressed 👍 |
Awesome! I'd do it myself, but I can't push to your |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
boom. tests are passing, thanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 This PR is included in version 1.91.2 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Was learning more about solana and saw solana-web3 to be interesting! |
Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up. |
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.Added
createKeyPairFromBytes_DANGEROUSLY_SKIP_VALIDATION
that skips the validation above ^Adds error code
SOLANA_ERROR__KEYS__VERIFY_SIGNATURE_FAILURE
Included in this PR is testcases to test the possible errors within these functions and creating a succesful keypair from a 64-byte array.
Closes #2289.