1
0
Fork 0
mirror of https://github.com/zhaofengli/attic.git synced 2024-12-14 11:57:30 +00:00

Support pubkey-only JWT configuration

This commit is contained in:
Cole Helbling 2024-02-26 11:44:04 -08:00
parent 0a9d4938ef
commit 756fef8d5f
2 changed files with 88 additions and 15 deletions

View file

@ -15,7 +15,8 @@ use serde::{de, Deserialize};
use xdg::BaseDirectories;
use crate::access::{
decode_token_hs256_secret_base64, decode_token_rs256_secret_base64, HS256Key, RS256KeyPair,
decode_token_hs256_secret_base64, decode_token_rs256_pubkey_base64,
decode_token_rs256_secret_base64, HS256Key, RS256KeyPair, RS256PublicKey,
};
use crate::narinfo::Compression as NixCompression;
use crate::storage::{LocalStorageConfig, S3StorageConfig};
@ -38,6 +39,10 @@ const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BAS
/// verifying received JWTs).
const ENV_TOKEN_RS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64";
/// Environment variable storing the base64-encoded RSA PEM PKCS1 public key (used for verifying
/// received JWTs only).
const ENV_TOKEN_RS256_PUBKEY_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_PUBKEY_BASE64";
/// Environment variable storing the database connection string.
const ENV_DATABASE_URL: &str = "ATTIC_SERVER_DATABASE_URL";
@ -141,20 +146,22 @@ pub struct JWTConfig {
#[serde(default = "Default::default")]
pub token_bound_audiences: Option<HashSet<String>>,
/// JSON Web Token signing.
#[serde(rename = "signing")]
#[serde(default = "load_jwt_signing_config_from_env")]
#[derivative(Debug = "ignore")]
pub signing_config: JWTSigningConfig,
}
/// JSON Web Token signing configuration.
#[derive(Clone, Deserialize)]
pub enum JWTSigningConfig {
/// JSON Web Token HMAC secret.
/// JSON Web Token RSA pubkey.
///
/// Set this to the base64-encoded HMAC secret to use for signing and verifying JWTs.
#[serde(rename = "token-hs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_hs256_secret_base64")]
HS256SignAndVerify(HS256Key),
/// Set this to the base64-encoded RSA PEM PKCS1 public key to use for verifying JWTs only.
#[serde(rename = "token-rs256-pubkey-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_pubkey_base64")]
RS256VerifyOnly(RS256PublicKey),
/// JSON Web Token RSA secret.
///
@ -163,13 +170,21 @@ pub enum JWTSigningConfig {
#[serde(rename = "token-rs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_rs256_secret_base64")]
RS256SignAndVerify(RS256KeyPair),
/// JSON Web Token HMAC secret.
///
/// Set this to the base64-encoded HMAC secret to use for signing and verifying JWTs.
#[serde(rename = "token-hs256-secret-base64")]
#[serde(deserialize_with = "deserialize_token_hs256_secret_base64")]
HS256SignAndVerify(HS256Key),
}
impl From<JWTSigningConfig> for SignatureType {
fn from(value: JWTSigningConfig) -> Self {
match value {
JWTSigningConfig::HS256SignAndVerify(key) => Self::HS256(key),
JWTSigningConfig::RS256VerifyOnly(key) => Self::RS256PubkeyOnly(key),
JWTSigningConfig::RS256SignAndVerify(key) => Self::RS256(key),
JWTSigningConfig::HS256SignAndVerify(key) => Self::HS256(key),
}
}
}
@ -297,28 +312,45 @@ pub struct GarbageCollectionConfig {
}
fn load_jwt_signing_config_from_env() -> JWTSigningConfig {
let config = if let Some(config) = load_token_rs256_secret_from_env() {
let config = if let Some(config) = load_token_rs256_pubkey_from_env() {
config
} else if let Some(config) = load_token_rs256_secret_from_env() {
config
} else if let Some(config) = load_token_hs256_secret_from_env() {
config
} else {
panic!(
"\n\
You must configure JWT signing and verification inside the [jwt.signing] block with \
one of the following settings:\n\
You must configure JWT signing and verification inside your TOML \
configuration by setting one of the following options in the \
[jwt.signing] block:\n\
\n\
* token-rs256-pubkey-base64\n\
* token-rs256-secret-base64\n\
* token-hs256-secret-base64\n\
\n\
or by setting one of the following environment variables:\n\
\n\
* {ENV_TOKEN_RS256_PUBKEY_BASE64}\n\
* {ENV_TOKEN_RS256_SECRET_BASE64}\n\
* {ENV_TOKEN_HS256_SECRET_BASE64}\n\
\n\
An RS256 secret will be used for both signing new JWTs and verifying received JWTs \
with the provided RSA (asymmetric) PEM PKCS1 private key.\n\
An HS256 secret will be used for both signing new JWTs and verifying received JWTs \
with the provided HMAC (symmetric) secret.\n\
Options will be tried in that same order (configuration options \
first, then environment options if none of the configuration options \
were set, starting with the respective RSA pubkey option, the RSA \
secret option, and finally the HMAC secret option). \
The first option that is found will be used.\n\
\n\
If an RS256 pubkey (asymmetric RSA PEM PKCS1 public key) is \
provided, it will only be possible to verify received JWTs, and not \
sign new JWTs.\n\
\n\
If an RS256 secret (asymmetric RSA PEM PKCS1 private key) is \
provided, it will be used for both signing new JWTs and verifying \
received JWTs.\n\
\n\
If an HS256 secret (symmetric HMAC secret) is provided, it will be \
used for both signing new JWTs and verifying received JWTs.\n\
"
)
};
@ -342,6 +374,14 @@ fn load_token_rs256_secret_from_env() -> Option<JWTSigningConfig> {
.map(JWTSigningConfig::RS256SignAndVerify)
}
fn load_token_rs256_pubkey_from_env() -> Option<JWTSigningConfig> {
let s = env::var(ENV_TOKEN_RS256_PUBKEY_BASE64).ok()?;
decode_token_rs256_pubkey_base64(&s)
.ok()
.map(JWTSigningConfig::RS256VerifyOnly)
}
fn load_database_url_from_env() -> String {
env::var(ENV_DATABASE_URL).expect(&format!(
"Database URL must be specified in either database.url \
@ -417,6 +457,20 @@ where
Ok(key)
}
fn deserialize_token_rs256_pubkey_base64<'de, D>(
deserializer: D,
) -> Result<RS256PublicKey, D::Error>
where
D: de::Deserializer<'de>,
{
use de::Error;
let s = String::deserialize(deserializer)?;
let key = decode_token_rs256_pubkey_base64(&s).map_err(Error::custom)?;
Ok(key)
}
fn default_listen_address() -> SocketAddr {
"[::]:8080".parse().unwrap()
}

View file

@ -91,7 +91,7 @@ use chrono::{DateTime, Utc};
use displaydoc::Display;
use jwt_simple::prelude::{Duration, RSAKeyPairLike, RSAPublicKeyLike, VerificationOptions};
pub use jwt_simple::{
algorithms::{HS256Key, MACLike, RS256KeyPair},
algorithms::{HS256Key, MACLike, RS256KeyPair, RS256PublicKey},
claims::{Claims, JWTClaims},
prelude::UnixTimeStamp,
};
@ -230,12 +230,16 @@ pub enum Error {
/// Failure decoding the base64 layer of the base64 encoded PEM
Utf8Error(std::str::Utf8Error),
/// Pubkey-only JWT authentication cannot create signed JWTs
PubkeyOnlyCannotCreateToken,
}
/// The supported JWT signature types.
pub enum SignatureType {
HS256(HS256Key),
RS256(RS256KeyPair),
RS256PubkeyOnly(RS256PublicKey),
}
impl Token {
@ -276,6 +280,10 @@ impl Token {
.map_err(Error::TokenError)
.map(Token)
}
SignatureType::RS256PubkeyOnly(key) => key
.verify_token(token, Some(opts))
.map_err(Error::TokenError)
.map(Token),
}
}
@ -324,6 +332,9 @@ impl Token {
match signature_type {
SignatureType::HS256(key) => key.authenticate(token).map_err(Error::TokenError),
SignatureType::RS256(key) => key.sign(token).map_err(Error::TokenError),
SignatureType::RS256PubkeyOnly(_) => {
return Err(Error::PubkeyOnlyCannotCreateToken);
}
}
}
@ -442,3 +453,11 @@ pub fn decode_token_rs256_secret_base64(s: &str) -> Result<RS256KeyPair> {
Ok(keypair)
}
pub fn decode_token_rs256_pubkey_base64(s: &str) -> Result<RS256PublicKey> {
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
let pubkey = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
let pubkey = RS256PublicKey::from_pem(pubkey).map_err(Error::TokenError)?;
Ok(pubkey)
}