feat(snowflake): derive public key from private key when omitted (WIN-1959) (#9251)

* feat(snowflake): derive public key from private key when omitted (WIN-1959)

Snowflake key-pair auth needs a SHA256 fingerprint of the public key for
the JWT iss claim, but the public key is mathematically derivable from
the RSA private key. Other tools (e.g. Power BI) only require the
private key, so requiring users to supply both is redundant. When
public_key is missing, fall back to deriving it from private_key (PKCS#8
or PKCS#1 PEM) instead of erroring out.

Fixes WIN-1959

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snowflake): treat empty public_key/private_key as missing

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-20 06:48:10 +00:00
committed by GitHub
parent ef0cb49f74
commit aa12c66c25
3 changed files with 45 additions and 7 deletions
+1
View File
@@ -15667,6 +15667,7 @@ dependencies = [
"regex",
"reqwest 0.13.1",
"reqwest-middleware",
"rsa",
"rust_decimal",
"serde",
"serde_json",
+2 -1
View File
@@ -13,7 +13,7 @@ default = []
private = ["windmill-worker-volumes/private", "windmill-queue/private", "windmill-common/private", "windmill-dep-map/private", "windmill-runtime-nativets?/private"]
mcp = ["windmill-ai/mcp", "dep:windmill-mcp"]
prometheus = ["dep:prometheus", "windmill-common/prometheus"]
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker-volumes/enterprise", "windmill-runtime-nativets?/enterprise", "dep:pem", "dep:tokio-util", "dep:opentelemetry-proto", "dep:prost", "dep:hudsucker", "dep:rcgen", "dep:hyper-http-proxy", "dep:hyper-tls", "dep:hyper-util"]
enterprise = ["windmill-queue/enterprise", "windmill-git-sync/enterprise", "windmill-common/enterprise", "windmill-worker-volumes/enterprise", "windmill-runtime-nativets?/enterprise", "dep:pem", "dep:rsa", "dep:tokio-util", "dep:opentelemetry-proto", "dep:prost", "dep:hudsucker", "dep:rcgen", "dep:hyper-http-proxy", "dep:hyper-tls", "dep:hyper-util"]
mssql = ["dep:tiberius"]
mssql-kerberos = ["mssql", "tiberius/integrated-auth-gssapi"] # Linux/Unix integrated auth
mssql-winauth = ["mssql", "tiberius/winauth"] # Windows integrated auth
@@ -112,6 +112,7 @@ jsonwebtoken.workspace = true
sha2.workspace = true
hmac.workspace = true
pem = { workspace = true, optional = true }
rsa = { workspace = true, optional = true }
urlencoding.workspace = true
nix.workspace = true
bytes.workspace = true
@@ -630,14 +630,50 @@ pub async fn do_snowflake(
)
.to_uppercase();
let public_key = match database.public_key.as_deref() {
Some(key) => pem::parse(key.as_bytes()).map_err(|e| {
Error::ExecutionErr(format!("Failed to parse public key: {}", e.to_string()))
})?,
None => return Err(Error::ExecutionErr("Public key is missing".to_string())),
let public_key_der: Vec<u8> = match database
.public_key
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
Some(key) => pem::parse(key.as_bytes())
.map_err(|e| Error::ExecutionErr(format!("Failed to parse public key: {e}")))?
.into_contents(),
None => {
// Derive the public key from the private key — RSA private keys
// contain the public components (n, e).
use rsa::pkcs8::{DecodePrivateKey, EncodePublicKey};
let pk_pem = database
.private_key
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| {
Error::ExecutionErr(
"Either public_key or private_key must be provided".to_string(),
)
})?;
let rsa_priv = rsa::RsaPrivateKey::from_pkcs8_pem(pk_pem)
.or_else(|_| {
use rsa::pkcs1::DecodeRsaPrivateKey;
rsa::RsaPrivateKey::from_pkcs1_pem(pk_pem)
})
.map_err(|e| {
Error::ExecutionErr(format!(
"Failed to parse private key to derive public key: {e}"
))
})?;
let rsa_pub = rsa::RsaPublicKey::from(&rsa_priv);
rsa_pub
.to_public_key_der()
.map_err(|e| {
Error::ExecutionErr(format!("Failed to encode derived public key: {e}"))
})?
.to_vec()
}
};
let mut public_key_hash = Sha256::new();
public_key_hash.update(public_key.contents());
public_key_hash.update(&public_key_der);
let public_key_fp = engine::general_purpose::STANDARD.encode(public_key_hash.finalize());