diff --git a/Cargo.lock b/Cargo.lock
index 5d2cdcea27..5c9170b7de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1416,6 +1416,7 @@ name = "control_plane"
version = "0.1.0"
dependencies = [
"anyhow",
+ "base64 0.13.1",
"camino",
"clap",
"comfy-table",
@@ -1425,10 +1426,13 @@ dependencies = [
"humantime",
"humantime-serde",
"hyper 0.14.30",
+ "jsonwebtoken",
"nix 0.27.1",
"once_cell",
"pageserver_api",
"pageserver_client",
+ "pem",
+ "pkcs8 0.10.2",
"postgres_backend",
"postgres_connection",
"regex",
@@ -1437,6 +1441,7 @@ dependencies = [
"scopeguard",
"serde",
"serde_json",
+ "sha2",
"storage_broker",
"thiserror 1.0.69",
"tokio",
@@ -2817,6 +2822,7 @@ dependencies = [
"hyper 0.14.30",
"itertools 0.10.5",
"jemalloc_pprof",
+ "jsonwebtoken",
"metrics",
"once_cell",
"pprof",
@@ -4269,6 +4275,7 @@ dependencies = [
"hyper 0.14.30",
"indoc",
"itertools 0.10.5",
+ "jsonwebtoken",
"md5",
"metrics",
"nix 0.27.1",
@@ -5685,9 +5692,9 @@ dependencies = [
[[package]]
name = "ring"
-version = "0.17.13"
+version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
@@ -5988,6 +5995,7 @@ dependencies = [
"humantime",
"hyper 0.14.30",
"itertools 0.10.5",
+ "jsonwebtoken",
"metrics",
"once_cell",
"pageserver_api",
@@ -7872,6 +7880,7 @@ dependencies = [
"metrics",
"nix 0.27.1",
"once_cell",
+ "pem",
"pin-project-lite",
"postgres_connection",
"pprof",
@@ -8460,6 +8469,7 @@ dependencies = [
"once_cell",
"p256 0.13.2",
"parquet",
+ "pkcs8 0.10.2",
"prettyplease",
"proc-macro2",
"prost 0.13.3",
diff --git a/Cargo.toml b/Cargo.toml
index d957fa9070..8fac3bb46c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -141,7 +141,9 @@ parking_lot = "0.12"
parquet = { version = "53", default-features = false, features = ["zstd"] }
parquet_derive = "53"
pbkdf2 = { version = "0.12.1", features = ["simple", "std"] }
+pem = "3.0.3"
pin-project-lite = "0.2"
+pkcs8 = "0.10.2"
pprof = { version = "0.14", features = ["criterion", "flamegraph", "frame-pointer", "prost-codec"] }
procfs = "0.16"
prometheus = {version = "0.13", default-features=false, features = ["process"]} # removes protobuf dependency
diff --git a/compute_tools/src/http/middleware/authorize.rs b/compute_tools/src/http/middleware/authorize.rs
index f221752c38..f1137de0ab 100644
--- a/compute_tools/src/http/middleware/authorize.rs
+++ b/compute_tools/src/http/middleware/authorize.rs
@@ -54,8 +54,8 @@ impl AsyncAuthorizeRequest
for Authorize {
Box::pin(async move {
let request_id = request.extract_parts::().await.unwrap();
- // TODO: Remove this stanza after teaching neon_local and the
- // regression tests to use a JWT + JWKS.
+ // TODO(tristan957): Remove this stanza after teaching neon_local
+ // and the regression tests to use a JWT + JWKS.
//
// https://github.com/neondatabase/neon/issues/11316
if cfg!(feature = "testing") {
@@ -112,6 +112,8 @@ impl Authorize {
token: &str,
validation: &Validation,
) -> Result> {
+ debug_assert!(!jwks.keys.is_empty());
+
debug!("verifying token {}", token);
for jwk in jwks.keys.iter() {
diff --git a/control_plane/Cargo.toml b/control_plane/Cargo.toml
index 162c49ec7c..a0ea216d9c 100644
--- a/control_plane/Cargo.toml
+++ b/control_plane/Cargo.toml
@@ -6,13 +6,17 @@ license.workspace = true
[dependencies]
anyhow.workspace = true
+base64.workspace = true
camino.workspace = true
clap.workspace = true
comfy-table.workspace = true
futures.workspace = true
humantime.workspace = true
+jsonwebtoken.workspace = true
nix.workspace = true
once_cell.workspace = true
+pem.workspace = true
+pkcs8.workspace = true
humantime-serde.workspace = true
hyper0.workspace = true
regex.workspace = true
@@ -20,6 +24,7 @@ reqwest = { workspace = true, features = ["blocking", "json"] }
scopeguard.workspace = true
serde.workspace = true
serde_json.workspace = true
+sha2.workspace = true
thiserror.workspace = true
toml.workspace = true
toml_edit.workspace = true
diff --git a/control_plane/src/bin/neon_local.rs b/control_plane/src/bin/neon_local.rs
index db9715dc62..950b264163 100644
--- a/control_plane/src/bin/neon_local.rs
+++ b/control_plane/src/bin/neon_local.rs
@@ -552,6 +552,7 @@ enum EndpointCmd {
Start(EndpointStartCmdArgs),
Reconfigure(EndpointReconfigureCmdArgs),
Stop(EndpointStopCmdArgs),
+ GenerateJwt(EndpointGenerateJwtCmdArgs),
}
#[derive(clap::Args)]
@@ -699,6 +700,13 @@ struct EndpointStopCmdArgs {
mode: String,
}
+#[derive(clap::Args)]
+#[clap(about = "Generate a JWT for an endpoint")]
+struct EndpointGenerateJwtCmdArgs {
+ #[clap(help = "Postgres endpoint id")]
+ endpoint_id: String,
+}
+
#[derive(clap::Subcommand)]
#[clap(about = "Manage neon_local branch name mappings")]
enum MappingsCmd {
@@ -1528,6 +1536,16 @@ async fn handle_endpoint(subcmd: &EndpointCmd, env: &local_env::LocalEnv) -> Res
.with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
endpoint.stop(&args.mode, args.destroy)?;
}
+ EndpointCmd::GenerateJwt(args) => {
+ let endpoint_id = &args.endpoint_id;
+ let endpoint = cplane
+ .endpoints
+ .get(endpoint_id)
+ .with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
+ let jwt = endpoint.generate_jwt()?;
+
+ println!("{jwt}");
+ }
}
Ok(())
diff --git a/control_plane/src/endpoint.rs b/control_plane/src/endpoint.rs
index 2fa7a62f8f..0fe6975a6e 100644
--- a/control_plane/src/endpoint.rs
+++ b/control_plane/src/endpoint.rs
@@ -42,22 +42,29 @@ use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use std::sync::Arc;
-use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
+use std::time::{Duration, Instant};
use anyhow::{Context, Result, anyhow, bail};
-use compute_api::requests::ConfigurationRequest;
+use compute_api::requests::{ComputeClaims, ConfigurationRequest};
use compute_api::responses::{
- ComputeConfig, ComputeCtlConfig, ComputeStatus, ComputeStatusResponse,
+ ComputeConfig, ComputeCtlConfig, ComputeStatus, ComputeStatusResponse, TlsConfig,
};
use compute_api::spec::{
Cluster, ComputeAudit, ComputeFeature, ComputeMode, ComputeSpec, Database, PgIdent,
RemoteExtSpec, Role,
};
+use jsonwebtoken::jwk::{
+ AlgorithmParameters, CommonParameters, EllipticCurve, Jwk, JwkSet, KeyAlgorithm, KeyOperations,
+ OctetKeyPairParameters, OctetKeyPairType, PublicKeyUse,
+};
use nix::sys::signal::{Signal, kill};
use pageserver_api::shard::ShardStripeSize;
+use pem::Pem;
+use pkcs8::der::Decode;
use reqwest::header::CONTENT_TYPE;
use safekeeper_api::membership::SafekeeperGeneration;
use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
use tracing::debug;
use url::Host;
use utils::id::{NodeId, TenantId, TimelineId};
@@ -82,6 +89,7 @@ pub struct EndpointConf {
drop_subscriptions_before_start: bool,
features: Vec,
cluster: Option,
+ compute_ctl_config: ComputeCtlConfig,
}
//
@@ -137,6 +145,36 @@ impl ComputeControlPlane {
.unwrap_or(self.base_port)
}
+ /// Create a JSON Web Key Set. This ideally matches the way we create a JWKS
+ /// from the production control plane.
+ fn create_jwks_from_pem(pem: Pem) -> Result {
+ let document = pkcs8::Document::from_der(&pem.into_contents())?;
+
+ let mut hasher = Sha256::new();
+ hasher.update(&document);
+ let key_hash = hasher.finalize();
+
+ Ok(JwkSet {
+ keys: vec![Jwk {
+ common: CommonParameters {
+ public_key_use: Some(PublicKeyUse::Signature),
+ key_operations: Some(vec![KeyOperations::Verify]),
+ key_algorithm: Some(KeyAlgorithm::EdDSA),
+ key_id: Some(base64::encode_config(key_hash, base64::URL_SAFE_NO_PAD)),
+ x509_url: None::,
+ x509_chain: None::>,
+ x509_sha1_fingerprint: None::,
+ x509_sha256_fingerprint: None::,
+ },
+ algorithm: AlgorithmParameters::OctetKeyPair(OctetKeyPairParameters {
+ key_type: OctetKeyPairType::OctetKeyPair,
+ curve: EllipticCurve::Ed25519,
+ x: base64::encode_config(&document, base64::URL_SAFE_NO_PAD),
+ }),
+ }],
+ })
+ }
+
#[allow(clippy::too_many_arguments)]
pub fn new_endpoint(
&mut self,
@@ -154,6 +192,10 @@ impl ComputeControlPlane {
let pg_port = pg_port.unwrap_or_else(|| self.get_port());
let external_http_port = external_http_port.unwrap_or_else(|| self.get_port() + 1);
let internal_http_port = internal_http_port.unwrap_or_else(|| external_http_port + 1);
+ let compute_ctl_config = ComputeCtlConfig {
+ jwks: Self::create_jwks_from_pem(self.env.read_public_key()?)?,
+ tls: None::,
+ };
let ep = Arc::new(Endpoint {
endpoint_id: endpoint_id.to_owned(),
pg_address: SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), pg_port),
@@ -181,6 +223,7 @@ impl ComputeControlPlane {
reconfigure_concurrency: 1,
features: vec![],
cluster: None,
+ compute_ctl_config: compute_ctl_config.clone(),
});
ep.create_endpoint_dir()?;
@@ -200,6 +243,7 @@ impl ComputeControlPlane {
reconfigure_concurrency: 1,
features: vec![],
cluster: None,
+ compute_ctl_config,
})?,
)?;
std::fs::write(
@@ -242,7 +286,6 @@ impl ComputeControlPlane {
///////////////////////////////////////////////////////////////////////////////
-#[derive(Debug)]
pub struct Endpoint {
/// used as the directory name
endpoint_id: String,
@@ -271,6 +314,9 @@ pub struct Endpoint {
features: Vec,
// Cluster settings
cluster: Option,
+
+ /// The compute_ctl config for the endpoint's compute.
+ compute_ctl_config: ComputeCtlConfig,
}
#[derive(PartialEq, Eq)]
@@ -333,6 +379,7 @@ impl Endpoint {
drop_subscriptions_before_start: conf.drop_subscriptions_before_start,
features: conf.features,
cluster: conf.cluster,
+ compute_ctl_config: conf.compute_ctl_config,
})
}
@@ -580,6 +627,13 @@ impl Endpoint {
Ok(safekeeper_connstrings)
}
+ /// Generate a JWT with the correct claims.
+ pub fn generate_jwt(&self) -> Result {
+ self.env.generate_auth_token(&ComputeClaims {
+ compute_id: self.endpoint_id.clone(),
+ })
+ }
+
#[allow(clippy::too_many_arguments)]
pub async fn start(
&self,
@@ -706,7 +760,7 @@ impl Endpoint {
ComputeConfig {
spec: Some(spec),
- compute_ctl_config: ComputeCtlConfig::default(),
+ compute_ctl_config: self.compute_ctl_config.clone(),
}
};
@@ -774,16 +828,7 @@ impl Endpoint {
])
// TODO: It would be nice if we generated compute IDs with the same
// algorithm as the real control plane.
- .args([
- "--compute-id",
- &format!(
- "compute-{}",
- SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .unwrap()
- .as_secs()
- ),
- ])
+ .args(["--compute-id", &self.endpoint_id])
.stdin(std::process::Stdio::null())
.stderr(logfile.try_clone()?)
.stdout(logfile);
@@ -881,6 +926,7 @@ impl Endpoint {
self.external_http_address.port()
),
)
+ .bearer_auth(self.generate_jwt()?)
.send()
.await?;
@@ -957,6 +1003,7 @@ impl Endpoint {
self.external_http_address.port()
))
.header(CONTENT_TYPE.as_str(), "application/json")
+ .bearer_auth(self.generate_jwt()?)
.body(
serde_json::to_string(&ConfigurationRequest {
spec,
diff --git a/control_plane/src/local_env.rs b/control_plane/src/local_env.rs
index fa10abe91a..b7906e5f81 100644
--- a/control_plane/src/local_env.rs
+++ b/control_plane/src/local_env.rs
@@ -12,6 +12,7 @@ use std::{env, fs};
use anyhow::{Context, bail};
use clap::ValueEnum;
+use pem::Pem;
use postgres_backend::AuthType;
use reqwest::Url;
use serde::{Deserialize, Serialize};
@@ -56,6 +57,7 @@ pub struct LocalEnv {
// used to issue tokens during e.g pg start
pub private_key_path: PathBuf,
+ /// Path to environment's public key
pub public_key_path: PathBuf,
pub broker: NeonBroker,
@@ -758,11 +760,11 @@ impl LocalEnv {
// this function is used only for testing purposes in CLI e g generate tokens during init
pub fn generate_auth_token(&self, claims: &S) -> anyhow::Result {
- let private_key_path = self.get_private_key_path();
- let key_data = fs::read(private_key_path)?;
- encode_from_key_file(claims, &key_data)
+ let key = self.read_private_key()?;
+ encode_from_key_file(claims, &key)
}
+ /// Get the path to the private key.
pub fn get_private_key_path(&self) -> PathBuf {
if self.private_key_path.is_absolute() {
self.private_key_path.to_path_buf()
@@ -771,6 +773,29 @@ impl LocalEnv {
}
}
+ /// Get the path to the public key.
+ pub fn get_public_key_path(&self) -> PathBuf {
+ if self.public_key_path.is_absolute() {
+ self.public_key_path.to_path_buf()
+ } else {
+ self.base_data_dir.join(&self.public_key_path)
+ }
+ }
+
+ /// Read the contents of the private key file.
+ pub fn read_private_key(&self) -> anyhow::Result {
+ let private_key_path = self.get_private_key_path();
+ let pem = pem::parse(fs::read(private_key_path)?)?;
+ Ok(pem)
+ }
+
+ /// Read the contents of the public key file.
+ pub fn read_public_key(&self) -> anyhow::Result {
+ let public_key_path = self.get_public_key_path();
+ let pem = pem::parse(fs::read(public_key_path)?)?;
+ Ok(pem)
+ }
+
/// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`].
pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> {
let base_path = base_path();
@@ -956,6 +981,7 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow
String::from_utf8_lossy(&keygen_output.stderr)
);
}
+
// Extract the public key from the private key file
//
// openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem
@@ -972,6 +998,7 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow
String::from_utf8_lossy(&keygen_output.stderr)
);
}
+
Ok(())
}
diff --git a/control_plane/src/storage_controller.rs b/control_plane/src/storage_controller.rs
index a4b56ae5c0..62ad5fa8d6 100644
--- a/control_plane/src/storage_controller.rs
+++ b/control_plane/src/storage_controller.rs
@@ -18,6 +18,7 @@ use pageserver_api::models::{
};
use pageserver_api::shard::TenantShardId;
use pageserver_client::mgmt_api::ResponseErrorMessageExt;
+use pem::Pem;
use postgres_backend::AuthType;
use reqwest::{Certificate, Method};
use serde::de::DeserializeOwned;
@@ -34,8 +35,8 @@ use crate::local_env::{LocalEnv, NeonStorageControllerConf};
pub struct StorageController {
env: LocalEnv,
- private_key: Option>,
- public_key: Option,
+ private_key: Option,
+ public_key: Option,
client: reqwest::Client,
config: NeonStorageControllerConf,
@@ -116,7 +117,9 @@ impl StorageController {
AuthType::Trust => (None, None),
AuthType::NeonJWT => {
let private_key_path = env.get_private_key_path();
- let private_key = fs::read(private_key_path).expect("failed to read private key");
+ let private_key =
+ pem::parse(fs::read(private_key_path).expect("failed to read private key"))
+ .expect("failed to parse PEM file");
// If pageserver auth is enabled, this implicitly enables auth for this service,
// using the same credentials.
@@ -138,9 +141,13 @@ impl StorageController {
.expect("Empty key dir")
.expect("Error reading key dir");
- std::fs::read_to_string(dent.path()).expect("Can't read public key")
+ pem::parse(std::fs::read_to_string(dent.path()).expect("Can't read public key"))
+ .expect("Failed to parse PEM file")
} else {
- std::fs::read_to_string(&public_key_path).expect("Can't read public key")
+ pem::parse(
+ std::fs::read_to_string(&public_key_path).expect("Can't read public key"),
+ )
+ .expect("Failed to parse PEM file")
};
(Some(private_key), Some(public_key))
}
diff --git a/libs/compute_api/src/responses.rs b/libs/compute_api/src/responses.rs
index 353949736b..b7d6b7ca34 100644
--- a/libs/compute_api/src/responses.rs
+++ b/libs/compute_api/src/responses.rs
@@ -160,7 +160,7 @@ pub struct CatalogObjects {
pub databases: Vec,
}
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ComputeCtlConfig {
/// Set of JSON web keys that the compute can use to authenticate
/// communication from the control plane.
@@ -179,7 +179,7 @@ impl Default for ComputeCtlConfig {
}
}
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct TlsConfig {
pub key_path: String,
pub cert_path: String,
diff --git a/libs/http-utils/Cargo.toml b/libs/http-utils/Cargo.toml
index 5f6578f76e..ab9380089b 100644
--- a/libs/http-utils/Cargo.toml
+++ b/libs/http-utils/Cargo.toml
@@ -14,6 +14,7 @@ futures.workspace = true
hyper0.workspace = true
itertools.workspace = true
jemalloc_pprof.workspace = true
+jsonwebtoken.workspace = true
once_cell.workspace = true
pprof.workspace = true
regex.workspace = true
diff --git a/libs/http-utils/src/endpoint.rs b/libs/http-utils/src/endpoint.rs
index 5588f6d87e..64147f2dd0 100644
--- a/libs/http-utils/src/endpoint.rs
+++ b/libs/http-utils/src/endpoint.rs
@@ -8,6 +8,7 @@ use bytes::{Bytes, BytesMut};
use hyper::header::{AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_TYPE, HeaderName};
use hyper::http::HeaderValue;
use hyper::{Body, Method, Request, Response};
+use jsonwebtoken::TokenData;
use metrics::{Encoder, IntCounter, TextEncoder, register_int_counter};
use once_cell::sync::Lazy;
use pprof::ProfilerGuardBuilder;
@@ -618,7 +619,7 @@ pub fn auth_middleware(
})?;
let token = parse_token(header_value)?;
- let data = auth.decode(token).map_err(|err| {
+ let data: TokenData = auth.decode(token).map_err(|err| {
warn!("Authentication error: {err}");
// Rely on From for ApiError impl
err
diff --git a/libs/utils/Cargo.toml b/libs/utils/Cargo.toml
index fd2fa63fd0..7b1dc56071 100644
--- a/libs/utils/Cargo.toml
+++ b/libs/utils/Cargo.toml
@@ -29,6 +29,7 @@ futures = { workspace = true }
jsonwebtoken.workspace = true
nix = { workspace = true, features = ["ioctl"] }
once_cell.workspace = true
+pem.workspace = true
pin-project-lite.workspace = true
regex.workspace = true
serde.workspace = true
diff --git a/libs/utils/src/auth.rs b/libs/utils/src/auth.rs
index db4fc5685c..de3a964d23 100644
--- a/libs/utils/src/auth.rs
+++ b/libs/utils/src/auth.rs
@@ -11,7 +11,8 @@ use camino::Utf8Path;
use jsonwebtoken::{
Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode,
};
-use serde::{Deserialize, Serialize};
+use pem::Pem;
+use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::id::TenantId;
@@ -73,7 +74,10 @@ impl SwappableJwtAuth {
pub fn swap(&self, jwt_auth: JwtAuth) {
self.0.swap(Arc::new(jwt_auth));
}
- pub fn decode(&self, token: &str) -> std::result::Result, AuthError> {
+ pub fn decode(
+ &self,
+ token: &str,
+ ) -> std::result::Result, AuthError> {
self.0.load().decode(token)
}
}
@@ -148,7 +152,10 @@ impl JwtAuth {
/// The function tries the stored decoding keys in succession,
/// and returns the first yielding a successful result.
/// If there is no working decoding key, it returns the last error.
- pub fn decode(&self, token: &str) -> std::result::Result, AuthError> {
+ pub fn decode(
+ &self,
+ token: &str,
+ ) -> std::result::Result, AuthError> {
let mut res = None;
for decoding_key in &self.decoding_keys {
res = Some(decode(token, decoding_key, &self.validation));
@@ -173,8 +180,8 @@ impl std::fmt::Debug for JwtAuth {
}
// this function is used only for testing purposes in CLI e g generate tokens during init
-pub fn encode_from_key_file(claims: &S, key_data: &[u8]) -> Result {
- let key = EncodingKey::from_ed_pem(key_data)?;
+pub fn encode_from_key_file(claims: &S, pem: &Pem) -> Result {
+ let key = EncodingKey::from_ed_der(pem.contents());
Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?)
}
@@ -188,13 +195,13 @@ mod tests {
//
// openssl genpkey -algorithm ed25519 -out ed25519-priv.pem
// openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem
- const TEST_PUB_KEY_ED25519: &[u8] = br#"
+ const TEST_PUB_KEY_ED25519: &str = r#"
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w=
-----END PUBLIC KEY-----
"#;
- const TEST_PRIV_KEY_ED25519: &[u8] = br#"
+ const TEST_PRIV_KEY_ED25519: &str = r#"
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
-----END PRIVATE KEY-----
@@ -222,9 +229,9 @@ MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
// Check it can be validated with the public key
let auth = JwtAuth::new(vec![
- DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap(),
+ DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(),
]);
- let claims_from_token = auth.decode(encoded_eddsa).unwrap().claims;
+ let claims_from_token: Claims = auth.decode(encoded_eddsa).unwrap().claims;
assert_eq!(claims_from_token, expected_claims);
}
@@ -235,13 +242,14 @@ MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
scope: Scope::Tenant,
};
- let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_ED25519).unwrap();
+ let pem = pem::parse(TEST_PRIV_KEY_ED25519).unwrap();
+ let encoded = encode_from_key_file(&claims, &pem).unwrap();
// decode it back
let auth = JwtAuth::new(vec![
- DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap(),
+ DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(),
]);
- let decoded = auth.decode(&encoded).unwrap();
+ let decoded: TokenData = auth.decode(&encoded).unwrap();
assert_eq!(decoded.claims, claims);
}
diff --git a/pageserver/Cargo.toml b/pageserver/Cargo.toml
index 74f3fce6e5..5c5bab0642 100644
--- a/pageserver/Cargo.toml
+++ b/pageserver/Cargo.toml
@@ -35,6 +35,7 @@ humantime.workspace = true
humantime-serde.workspace = true
hyper0.workspace = true
itertools.workspace = true
+jsonwebtoken.workspace = true
md5.workspace = true
nix.workspace = true
# hack to get the number of worker threads tokio uses
diff --git a/pageserver/src/page_service.rs b/pageserver/src/page_service.rs
index 7a62d8049b..560ac75f4a 100644
--- a/pageserver/src/page_service.rs
+++ b/pageserver/src/page_service.rs
@@ -15,6 +15,7 @@ use async_compression::tokio::write::GzipEncoder;
use bytes::Buf;
use futures::FutureExt;
use itertools::Itertools;
+use jsonwebtoken::TokenData;
use once_cell::sync::OnceCell;
use pageserver_api::config::{
PageServicePipeliningConfig, PageServicePipeliningConfigPipelined,
@@ -2837,7 +2838,7 @@ where
) -> Result<(), QueryError> {
// this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT
// which requires auth to be present
- let data = self
+ let data: TokenData = self
.auth
.as_ref()
.unwrap()
diff --git a/safekeeper/Cargo.toml b/safekeeper/Cargo.toml
index 965aa7504b..a0ba69aa34 100644
--- a/safekeeper/Cargo.toml
+++ b/safekeeper/Cargo.toml
@@ -27,6 +27,7 @@ humantime.workspace = true
http.workspace = true
hyper0.workspace = true
itertools.workspace = true
+jsonwebtoken.workspace = true
futures.workspace = true
once_cell.workspace = true
parking_lot.workspace = true
diff --git a/safekeeper/src/handler.rs b/safekeeper/src/handler.rs
index 5ca3d1b7c2..b54bee8bfb 100644
--- a/safekeeper/src/handler.rs
+++ b/safekeeper/src/handler.rs
@@ -6,6 +6,7 @@ use std::str::{self, FromStr};
use std::sync::Arc;
use anyhow::Context;
+use jsonwebtoken::TokenData;
use pageserver_api::models::ShardParameters;
use pageserver_api::shard::{ShardIdentity, ShardStripeSize};
use postgres_backend::{PostgresBackend, QueryError};
@@ -278,7 +279,7 @@ impl postgres_backend::Handler
.auth
.as_ref()
.expect("auth_type is configured but .auth of handler is missing");
- let data = auth
+ let data: TokenData = auth
.decode(str::from_utf8(jwt_response).context("jwt response is not UTF-8")?)
.map_err(|e| QueryError::Unauthorized(e.0))?;
diff --git a/workspace_hack/Cargo.toml b/workspace_hack/Cargo.toml
index b548a2a88a..2c37cebc27 100644
--- a/workspace_hack/Cargo.toml
+++ b/workspace_hack/Cargo.toml
@@ -70,6 +70,7 @@ num-traits = { version = "0.2", features = ["i128", "libm"] }
once_cell = { version = "1" }
p256 = { version = "0.13", features = ["jwk"] }
parquet = { version = "53", default-features = false, features = ["zstd"] }
+pkcs8 = { version = "0.10", default-features = false, features = ["pem", "std"] }
prost = { version = "0.13", features = ["no-recursion-limit", "prost-derive"] }
rand = { version = "0.8", features = ["small_rng"] }
regex = { version = "1" }