Skip to content

Commit cd66f7c

Browse files
goffrieConvex, Inc.
authored and
Convex, Inc.
committed
Implement generateKey & other crypto operations for actions (#36130)
1. Remove `secure_rng_unavailable` and replace it with a wrapper `CryptoRng` which indicates that access to system randomness is allowed. This is only provided in the action environment. 2. Fix RSA-PSS verify to use the provided `saltLength`, which was otherwise incorrect when the value was non default. 3. Allow a number of operations that use SecureRandom but aren't actually nondeterministic - e.g. converting ECDSA private keys into public keys. (`ring` uses the random number generator to initialize some state for signing, but we don't always need to sign.) 4. Add new a crypto op for generating keypairs for RSA, ECDSA/ECDH, and Ed25519/X25519. These all use CryptoRng and are therefore available only in actions. Expose this operation from SubtleCrypto.generateKey. 5. Enable the relevant deno crypto tests & write another one for ed25519. 6. Fix Typescript typing for SubtleCrypto implementation. Most importantly I made the type of `algorithmNameLiteralWithParams` more precise so that we get proper literal types from `z.infer`. GitOrigin-RevId: a6775d39655bdcfe972d13f66c63ecfadf72fcaf
1 parent baae8e4 commit cd66f7c

File tree

24 files changed

+1240
-601
lines changed

24 files changed

+1240
-601
lines changed

crates/isolate/src/environment/action/mod.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,13 @@ use self::{
123123
},
124124
task_executor::TaskExecutor,
125125
};
126-
use super::warnings::{
127-
approaching_duration_limit_warning,
128-
approaching_limit_warning,
129-
SystemWarning,
126+
use super::{
127+
crypto_rng::CryptoRng,
128+
warnings::{
129+
approaching_duration_limit_warning,
130+
approaching_limit_warning,
131+
SystemWarning,
132+
},
130133
};
131134
use crate::{
132135
client::{
@@ -1364,6 +1367,10 @@ impl<RT: Runtime> IsolateEnvironment<RT> for ActionEnvironment<RT> {
13641367
self.phase.rng()
13651368
}
13661369

1370+
fn crypto_rng(&mut self) -> anyhow::Result<CryptoRng> {
1371+
Ok(CryptoRng::new())
1372+
}
1373+
13671374
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
13681375
self.phase.unix_timestamp()
13691376
}

crates/isolate/src/environment/analyze.rs

+7
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ impl<RT: Runtime> IsolateEnvironment<RT> for AnalyzeEnvironment {
146146
Ok(&mut self.rng)
147147
}
148148

149+
fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> {
150+
anyhow::bail!(ErrorMetadata::bad_request(
151+
"NoCryptoRngDuringImport",
152+
"Cannot use cryptographic randomness at import time"
153+
))
154+
}
155+
149156
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
150157
Ok(self.unix_timestamp)
151158
}

crates/isolate/src/environment/auth_config.rs

+7
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ impl<RT: Runtime> IsolateEnvironment<RT> for AuthConfigEnvironment {
8383
))
8484
}
8585

86+
fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> {
87+
anyhow::bail!(ErrorMetadata::bad_request(
88+
"NoCryptoRngDuringAuthConfig",
89+
"Cannot use cryptographic randomness when evaluating auth config file"
90+
))
91+
}
92+
8693
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
8794
anyhow::bail!(ErrorMetadata::bad_request(
8895
"NoDateDuringAuthConfig",

crates/isolate/src/environment/component_definitions.rs

+7
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,13 @@ impl<RT: Runtime> IsolateEnvironment<RT> for DefinitionEnvironment {
470470
))
471471
}
472472

473+
fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> {
474+
anyhow::bail!(ErrorMetadata::bad_request(
475+
"NoCryptoRngDuringDefinitionEvaluation",
476+
"Cannot use cryptographic randomness when evaluating app definition"
477+
))
478+
}
479+
473480
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
474481
anyhow::bail!(ErrorMetadata::bad_request(
475482
"NoDateDuringDefinitionEvaluation",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/// Represents access to an RNG suitable for cryptographic operations like key
2+
/// generation, i.e. system randomness.
3+
///
4+
/// This is unavailable in deterministic UDFs (i.e. queries/mutations).
5+
pub struct CryptoRng(());
6+
7+
impl CryptoRng {
8+
pub fn new() -> Self {
9+
CryptoRng(())
10+
}
11+
12+
/// Returns a `ring`-compatible random number generator
13+
pub fn ring(&self) -> ring::rand::SystemRandom {
14+
ring::rand::SystemRandom::new()
15+
}
16+
17+
/// Returns an `rsa`-compatible random number generator
18+
pub fn rsa(&self) -> rsa::rand_core::OsRng {
19+
rsa::rand_core::OsRng
20+
}
21+
}

crates/isolate/src/environment/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ use model::{
77
},
88
modules::module_versions::FullModuleSource,
99
};
10+
11+
use self::crypto_rng::CryptoRng;
1012
pub mod action;
1113
pub mod analyze;
1214
pub mod async_op;
1315
pub mod auth_config;
1416
pub mod component_definitions;
17+
pub mod crypto_rng;
1518
pub mod helpers;
1619
pub mod schema;
1720
pub mod udf;
@@ -76,6 +79,7 @@ pub trait IsolateEnvironment<RT: Runtime>: 'static {
7679

7780
fn trace(&mut self, level: LogLevel, messages: Vec<String>) -> anyhow::Result<()>;
7881
fn rng(&mut self) -> anyhow::Result<&mut ChaCha12Rng>;
82+
fn crypto_rng(&mut self) -> anyhow::Result<CryptoRng>;
7983
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp>;
8084

8185
fn get_environment_variable(&mut self, name: EnvVarName)

crates/isolate/src/environment/schema.rs

+7
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ impl<RT: Runtime> IsolateEnvironment<RT> for SchemaEnvironment {
8484
Ok(&mut self.rng)
8585
}
8686

87+
fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> {
88+
anyhow::bail!(ErrorMetadata::bad_request(
89+
"NoCryptoRngInSchema",
90+
"Cannot use cryptographic randomness when evaluating schema"
91+
))
92+
}
93+
8794
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
8895
Ok(self.unix_timestamp)
8996
}

crates/isolate/src/environment/udf/mod.rs

+18-7
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ pub struct DatabaseUdfEnvironment<RT: Runtime> {
196196
udf_callback: Box<dyn UdfCallback<RT>>,
197197
}
198198

199+
fn not_allowed_in_udf(name: &str, description: &str) -> ErrorMetadata {
200+
ErrorMetadata::bad_request(
201+
format!("No{name}InQueriesOrMutations"),
202+
format!(
203+
"Can't use {description} in queries and mutations. Please consider using an action. \
204+
See https://docs.convex.dev/functions/actions for more details.",
205+
),
206+
)
207+
}
208+
199209
impl<RT: Runtime> IsolateEnvironment<RT> for DatabaseUdfEnvironment<RT> {
200210
fn trace(&mut self, level: LogLevel, messages: Vec<String>) -> anyhow::Result<()> {
201211
self.emit_log_line(LogLine::new_developer_log_line(
@@ -212,6 +222,10 @@ impl<RT: Runtime> IsolateEnvironment<RT> for DatabaseUdfEnvironment<RT> {
212222
self.phase.rng()
213223
}
214224

225+
fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> {
226+
anyhow::bail!(not_allowed_in_udf("CryptoRng", "cryptographic randomness"))
227+
}
228+
215229
fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> {
216230
self.phase.unix_timestamp()
217231
}
@@ -271,13 +285,10 @@ impl<RT: Runtime> IsolateEnvironment<RT> for DatabaseUdfEnvironment<RT> {
271285
request: AsyncOpRequest,
272286
_resolver: v8::Global<v8::PromiseResolver>,
273287
) -> anyhow::Result<()> {
274-
anyhow::bail!(ErrorMetadata::bad_request(
275-
format!("No{}InQueriesOrMutations", request.name_for_error()),
276-
format!(
277-
"Can't use {} in queries and mutations. Please consider using an action. See https://docs.convex.dev/functions/actions for more details.",
278-
request.description_for_error()
279-
),
280-
))
288+
anyhow::bail!(not_allowed_in_udf(
289+
request.name_for_error(),
290+
&request.description_for_error(),
291+
))
281292
}
282293

283294
fn record_heap_stats(&self, mut isolate_stats: IsolateHeapStats) {

crates/isolate/src/isolate2/callback_context.rs

+5
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,11 @@ mod op_provider {
480480
state.environment.rng()
481481
}
482482

483+
fn crypto_rng(&mut self) -> anyhow::Result<crate::environment::crypto_rng::CryptoRng> {
484+
// TODO: this needs to detect if we are in an action
485+
anyhow::bail!("TODO: CryptoRng in isolate2")
486+
}
487+
483488
fn scope(&mut self) -> &mut v8::HandleScope<'scope> {
484489
self.scope
485490
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use anyhow::Context as _;
2+
use deno_core::ToJsBuffer;
3+
use ring::{
4+
rand::SecureRandom,
5+
signature::{
6+
EcdsaKeyPair,
7+
Ed25519KeyPair,
8+
KeyPair,
9+
},
10+
};
11+
use rsa::pkcs1::{
12+
EncodeRsaPrivateKey,
13+
EncodeRsaPublicKey,
14+
};
15+
16+
use super::{
17+
shared::RustRawKeyData,
18+
Algorithm,
19+
CryptoOps,
20+
Curve25519Algorithm,
21+
GenerateKeypairAlgorithm,
22+
GeneratedKey,
23+
GeneratedKeypair,
24+
};
25+
use crate::environment::crypto_rng::CryptoRng;
26+
27+
impl CryptoOps {
28+
pub fn generate_keypair(
29+
rng: CryptoRng,
30+
algorithm: GenerateKeypairAlgorithm,
31+
) -> anyhow::Result<GeneratedKeypair> {
32+
match algorithm {
33+
GenerateKeypairAlgorithm::Rsa {
34+
name: Algorithm::RsaPss | Algorithm::RsaOaep | Algorithm::RsassaPkcs1v15,
35+
modulus_length,
36+
public_exponent,
37+
} => {
38+
let exp = rsa::BigUint::from_bytes_be(&public_exponent);
39+
let private_key =
40+
rsa::RsaPrivateKey::new_with_exp(&mut rng.rsa(), modulus_length, &exp)?;
41+
let public_key = private_key.to_public_key();
42+
Ok(GeneratedKeypair {
43+
private_raw_data: GeneratedKey::KeyData(RustRawKeyData::Private(
44+
private_key.to_pkcs1_der()?.as_bytes().to_vec().into(),
45+
)),
46+
public_raw_data: GeneratedKey::KeyData(RustRawKeyData::Public(
47+
public_key.to_pkcs1_der()?.into_vec().into(),
48+
)),
49+
})
50+
},
51+
GenerateKeypairAlgorithm::Ec {
52+
name: Algorithm::Ecdsa | Algorithm::Ecdh,
53+
named_curve,
54+
} => {
55+
let private_key_pkcs8 =
56+
EcdsaKeyPair::generate_pkcs8(named_curve.into(), &rng.ring())
57+
.ok()
58+
.context("failed to generate ecdsa keypair")?;
59+
let keypair = EcdsaKeyPair::from_pkcs8(
60+
named_curve.into(),
61+
private_key_pkcs8.as_ref(),
62+
&rng.ring(),
63+
)
64+
.ok()
65+
.context("failed to parse ecdsa pkcs8 that we just generated")?;
66+
Ok(GeneratedKeypair {
67+
private_raw_data: GeneratedKey::KeyData(RustRawKeyData::Private(
68+
// private key is PKCS#8-encoded
69+
private_key_pkcs8.as_ref().to_vec().into(),
70+
)),
71+
public_raw_data: GeneratedKey::KeyData(RustRawKeyData::Public(
72+
// public key is just the elliptic curve point
73+
keypair.public_key().as_ref().to_vec().into(),
74+
)),
75+
})
76+
},
77+
GenerateKeypairAlgorithm::Curve25519 {
78+
name: Curve25519Algorithm::Ed25519,
79+
} => {
80+
let pkcs8_keypair = Ed25519KeyPair::generate_pkcs8(&rng.ring())
81+
.ok()
82+
.context("failed to generate ed25519 key")?;
83+
// ring is really annoying and needs to jump through hoops to get the public key
84+
// that we just generated
85+
let public_key = Ed25519KeyPair::from_pkcs8(pkcs8_keypair.as_ref())
86+
.ok()
87+
.context("failed to parse ed25519 pkcs8 that we just generated")?
88+
.public_key()
89+
.as_ref()
90+
.to_vec();
91+
// ring is really really annoying and doesn't export the raw
92+
// seed at all, so use RustCrypto instead
93+
let private_key = Self::import_pkcs8_ed25519(pkcs8_keypair.as_ref())
94+
.context("failed to import ed25519 pkcs8 that we just generated")?;
95+
Ok(GeneratedKeypair {
96+
private_raw_data: GeneratedKey::RawBytes(private_key),
97+
public_raw_data: GeneratedKey::RawBytes(public_key.into()),
98+
})
99+
},
100+
GenerateKeypairAlgorithm::Curve25519 {
101+
name: Curve25519Algorithm::X25519,
102+
} => {
103+
// ring refuses to generate exportable X25519 keys
104+
// (this should be unreachable as X25519 is rejected in the UDF runtime as well)
105+
anyhow::bail!("TODO: not yet supported");
106+
},
107+
_ => anyhow::bail!("invalid algorithm"),
108+
}
109+
}
110+
111+
pub fn generate_key_bytes(rng: CryptoRng, length: usize) -> anyhow::Result<ToJsBuffer> {
112+
anyhow::ensure!(length <= 1024, "key too long");
113+
let mut buf = vec![0; length];
114+
rng.ring()
115+
.fill(&mut buf)
116+
.ok()
117+
.context("failed to generate random bytes")?;
118+
Ok(buf.into())
119+
}
120+
}

0 commit comments

Comments
 (0)