diff --git a/Cargo.lock b/Cargo.lock index a122544..cfd029f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,7 @@ dependencies = [ "jsonwebtoken", "lazy_static", "regex", + "rsa", "serde", "serde_with", "tracing", @@ -796,6 +797,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bigdecimal" version = "0.3.1" @@ -1143,6 +1150,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "const_format" version = "0.2.30" @@ -1330,6 +1343,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1360,6 +1384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -2099,6 +2124,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -2106,6 +2134,12 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libsqlite3-sys" version = "0.24.2" @@ -2280,6 +2314,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2290,6 +2341,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -2297,6 +2359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2409,6 +2472,15 @@ dependencies = [ "serde", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -2447,6 +2519,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -2782,6 +2875,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rsa" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust_decimal" version = "1.29.1" @@ -3286,6 +3399,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "simdutf8" version = "0.1.4" @@ -3344,6 +3467,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sqlformat" version = "0.2.1" diff --git a/book/src/admin-guide/deployment/nixos.md b/book/src/admin-guide/deployment/nixos.md index 27615d2..45ede64 100644 --- a/book/src/admin-guide/deployment/nixos.md +++ b/book/src/admin-guide/deployment/nixos.md @@ -11,16 +11,16 @@ Attic provides [a NixOS module](https://github.com/zhaofengli/attic/blob/main/ni ## Generating the Credentials File -The HS256 JWT secret can be generated with the `openssl` utility: +The RS256 JWT secret can be generated with the `openssl` utility: ```bash -openssl rand 64 | base64 -w0 +openssl genrsa -traditional -out private_key.pem 4096 ``` Create a file on the server containing the following contents: ``` -ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from openssl" +ATTIC_SERVER_TOKEN_RS256_SECRET="output from openssl" ``` Ensure the file is only accessible by root. diff --git a/integration-tests/basic/default.nix b/integration-tests/basic/default.nix index 2b029e1..2165f72 100644 --- a/integration-tests/basic/default.nix +++ b/integration-tests/basic/default.nix @@ -6,7 +6,7 @@ let cmd = { atticadm = "atticd-atticadm"; - atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticd -f ${serverConfigFile}"; + atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET && atticd -f ${serverConfigFile}"; }; makeTestDerivation = pkgs.writeShellScript "make-drv" '' @@ -125,7 +125,7 @@ in { # For testing only - Don't actually do this environment.etc."atticd.env".text = '' - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ=" + ATTIC_SERVER_TOKEN_RS256_SECRET="$(openssl genrsa -traditional -out - 512)" ''; services.atticd = { @@ -143,7 +143,7 @@ in { }; }; - environment.systemPackages = [ pkgs.attic-server ]; + environment.systemPackages = [ pkgs.openssl pkgs.attic-server ]; networking.firewall.allowedTCPPorts = [ 8080 ]; }; diff --git a/nixos/atticd.nix b/nixos/atticd.nix index cba4494..a33b415 100644 --- a/nixos/atticd.nix +++ b/nixos/atticd.nix @@ -16,7 +16,7 @@ let } '' cat $configFile - export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ=" + export ATTIC_SERVER_TOKEN_RS256_SECRET="$(${pkgs.openssl}/bin/openssl genrsa -traditional -out - 512)" export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:" ${cfg.package}/bin/atticd --mode check-config -f $configFile cat <$configFile >$out @@ -78,8 +78,8 @@ in Path to an EnvironmentFile containing required environment variables: - - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64: The Base64-encoded version of the - HS256 JWT secret. Generate it with `openssl rand 64 | base64 -w0`. + - ATTIC_SERVER_TOKEN_RS256_SECRET: The PEM-encoded version of the + RS256 JWT secret. Generate it with `openssl genrsa -traditional -out private_key.pem 4096`. ''; type = types.nullOr types.path; default = null; @@ -135,9 +135,9 @@ in message = '' is not set. - Run `openssl rand 64 | base64 -w0` and create a file with the following contents: + Run `openssl genrsa -traditional -out private_key.pem 4096` and create a file with the following contents: - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from command" + ATTIC_SERVER_TOKEN_RS256_SECRET="output from command" Then, set `services.atticd.credentialsFile` to the quoted absolute path of the file. ''; diff --git a/server/src/access/http.rs b/server/src/access/http.rs index e2b144f..d6e2326 100644 --- a/server/src/access/http.rs +++ b/server/src/access/http.rs @@ -101,7 +101,7 @@ pub async fn apply_auth(req: Request, next: Next) -> Response { .and_then(parse_authorization_header) .and_then(|jwt| { let state = req.extensions().get::().unwrap(); - let res_token = Token::from_jwt(&jwt, &state.config.token_hs256_secret.1); + let res_token = Token::from_jwt(&jwt, &state.config.token_rs256_secret.1); if let Err(e) = &res_token { tracing::debug!("Ignoring bad JWT token: {}", e); } diff --git a/server/src/adm/command/make_token.rs b/server/src/adm/command/make_token.rs index 5777564..592103b 100644 --- a/server/src/adm/command/make_token.rs +++ b/server/src/adm/command/make_token.rs @@ -115,7 +115,7 @@ pub async fn run(config: Config, opts: Opts) -> Result<()> { if sub.dump_claims { println!("{}", serde_json::to_string(token.opaque_claims())?); } else { - let encoded_token = token.encode(&config.token_hs256_secret.0)?; + let encoded_token = token.encode(&config.token_rs256_secret.0)?; println!("{}", encoded_token); } diff --git a/server/src/config-template.toml b/server/src/config-template.toml index 84127d2..ba67689 100644 --- a/server/src/config-template.toml +++ b/server/src/config-template.toml @@ -36,10 +36,10 @@ allowed-hosts = [] # JWT signing token # -# Set this to the Base64 encoding of some random data. -# You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64` environment +# Set this to the PEM encoding of some random data. +# You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET` environment # variable. -token-hs256-secret-base64 = "%token_hs256_secret_base64%" +token-rs256-secret = "%token_rs256_secret%" # Database connection [database] @@ -85,7 +85,7 @@ path = "%storage_path%" #[storage.credentials] # access_key_id = "" # secret_access_key = "" - + # Data chunking # # Warning: If you change any of the values here, it will be diff --git a/server/src/config.rs b/server/src/config.rs index 0c8a860..c3b605a 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -12,7 +12,7 @@ use derivative::Derivative; use serde::{de, Deserialize}; use xdg::BaseDirectories; -use crate::access::{decode_token_hs256_secret_base64, DecodingKey, EncodingKey}; +use crate::access::{decode_token_rs256_secret, DecodingKey, EncodingKey}; use crate::narinfo::Compression as NixCompression; use crate::storage::{LocalStorageConfig, S3StorageConfig}; @@ -26,8 +26,8 @@ const XDG_PREFIX: &str = "attic"; /// This is useful for deploying to certain application platforms like Fly.io const ENV_CONFIG_BASE64: &str = "ATTIC_SERVER_CONFIG_BASE64"; -/// Environment variable storing the Base64-encoded HS256 JWT secret. -const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64"; +/// Environment variable storing the PEM-encoded RS256 JWT secret. +const ENV_TOKEN_RS256_SECRET: &str = "ATTIC_SERVER_TOKEN_RS256_SECRET"; /// Environment variable storing the database connection string. const ENV_DATABASE_URL: &str = "ATTIC_SERVER_DATABASE_URL"; @@ -110,12 +110,12 @@ pub struct Config { /// JSON Web Token HMAC secret. /// - /// Set this to the base64 encoding of a randomly generated secret. - #[serde(rename = "token-hs256-secret-base64")] - #[serde(deserialize_with = "deserialize_token_hs256_secret_base64")] - #[serde(default = "load_token_hs256_secret_from_env")] + /// Set this to the PEM encoding of a randomly generated secret. + #[serde(rename = "token-rs256-secret")] + #[serde(deserialize_with = "deserialize_token_rs256_secret")] + #[serde(default = "load_token_rs256_secret_from_env")] #[derivative(Debug = "ignore")] - pub token_hs256_secret: (EncodingKey, DecodingKey), + pub token_rs256_secret: (EncodingKey, DecodingKey), } /// Database connection configuration. @@ -240,11 +240,11 @@ pub struct GarbageCollectionConfig { pub default_retention_period: Duration, } -fn load_token_hs256_secret_from_env() -> (EncodingKey, DecodingKey) { - let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64) - .expect("The HS256 secret must be specified in either token_hs256_secret or the ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 environment."); +fn load_token_rs256_secret_from_env() -> (EncodingKey, DecodingKey) { + let s = env::var(ENV_TOKEN_RS256_SECRET) + .expect("The RS256 secret must be specified in either token_rs256_secret or the ATTIC_SERVER_TOKEN_RS256_SECRET environment."); - decode_token_hs256_secret_base64(&s).expect("Failed to load as decoding key") + decode_token_rs256_secret(&s).expect("Failed to load as decoding key") } fn load_database_url_from_env() -> String { @@ -296,7 +296,7 @@ impl Default for GarbageCollectionConfig { } } -fn deserialize_token_hs256_secret_base64<'de, D>( +fn deserialize_token_rs256_secret<'de, D>( deserializer: D, ) -> Result<(EncodingKey, DecodingKey), D::Error> where @@ -305,7 +305,7 @@ where use de::Error; let s = String::deserialize(deserializer)?; - let key = decode_token_hs256_secret_base64(&s).map_err(Error::custom)?; + let key = decode_token_rs256_secret(&s).map_err(Error::custom)?; Ok(key) } diff --git a/server/src/oobe.rs b/server/src/oobe.rs index 488c1db..899d234 100644 --- a/server/src/oobe.rs +++ b/server/src/oobe.rs @@ -18,7 +18,7 @@ use rand::distributions::Alphanumeric; use rand::Rng; use tokio::fs::{self, OpenOptions}; -use crate::access::{decode_token_hs256_secret_base64, Token}; +use crate::access::{decode_token_rs256_secret, Token}; use crate::config; use attic::cache::CacheNamePattern; @@ -45,7 +45,7 @@ pub async fn run_oobe() -> Result<()> { let storage_path = data_path.join("storage"); fs::create_dir_all(&storage_path).await?; - let hs256_secret_base64 = { + let rs256_secret = { let random: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(128) @@ -58,7 +58,7 @@ pub async fn run_oobe() -> Result<()> { let config_content = CONFIG_TEMPLATE .replace("%database_url%", &database_url) .replace("%storage_path%", storage_path.to_str().unwrap()) - .replace("%token_hs256_secret_base64%", &hs256_secret_base64); + .replace("%token_rs256_secret%", &rs256_secret); fs::write(&config_path, config_content.as_bytes()).await?; @@ -76,7 +76,7 @@ pub async fn run_oobe() -> Result<()> { perm.configure_cache_retention = true; perm.destroy_cache = true; - let key = decode_token_hs256_secret_base64(&hs256_secret_base64).unwrap(); + let key = decode_token_rs256_secret(&rs256_secret).unwrap(); token.encode(&key.0)? }; diff --git a/token/Cargo.toml b/token/Cargo.toml index 2a5ad73..d49ac98 100644 --- a/token/Cargo.toml +++ b/token/Cargo.toml @@ -17,3 +17,4 @@ regex = "1.8.3" serde = "1.0.163" serde_with = "3.0.0" tracing = "0.1.37" +rsa = "0.9.3" diff --git a/token/src/lib.rs b/token/src/lib.rs index 80e35c5..3cfd46f 100644 --- a/token/src/lib.rs +++ b/token/src/lib.rs @@ -1,7 +1,7 @@ //! Access control. //! //! Access control in Attic is simple and stateless [0] - The server validates -//! the JWT against a HS256 key and allows access based on the `https://jwt.attic.rs/v1` +//! the JWT against a RS256 key and allows access based on the `https://jwt.attic.rs/v1` //! claim. //! //! One primary goal of the Attic Server is easy scalability. It's designed @@ -86,11 +86,11 @@ mod tests; use std::collections::HashMap; use std::error::Error as StdError; -use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; use chrono::{DateTime, Utc}; use displaydoc::Display; use jsonwebtoken::{Algorithm, Validation}; pub use jsonwebtoken::{DecodingKey, EncodingKey}; +use rsa::pkcs1::{DecodeRsaPrivateKey, EncodeRsaPublicKey}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, BoolFromInt}; @@ -272,8 +272,8 @@ pub enum Error { /// JWT error: {0} TokenError(jsonwebtoken::errors::Error), - /// Base64 decode error: {0} - Base64Error(base64::DecodeError), + /// RSA Key error: {0} + RsaKeyError(rsa::pkcs1::Error), } impl Token { @@ -281,13 +281,13 @@ impl Token { pub fn from_jwt(token: &str, key: &jsonwebtoken::DecodingKey) -> Result { // TODO: create a static validator for us so we don't have to construct a new one every time? - let mut validation = Validation::new(Algorithm::HS256); + let mut validation = Validation::new(Algorithm::RS256); validation.validate_nbf = true; // validation.set_issuer(&[ctx.config.flakehub_jwt_bound_issuer.clone()]); // validation.set_audience(&[ctx.config.jwt_bound_audience.clone()]); validation.set_required_spec_claims(&["exp", "nbf", "aud", "iss", "sub"]); - jsonwebtoken::decode::>(token, &key, &validation) + jsonwebtoken::decode::>(token, key, &validation) .map_err(Error::TokenError) .map(|tokendata| tokendata.claims) .map(Token) @@ -420,12 +420,18 @@ impl CachePermission { impl StdError for Error {} -pub fn decode_token_hs256_secret_base64(s: &str) -> Result<(EncodingKey, DecodingKey)> { - let secret = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?; - Ok(( - EncodingKey::from_secret(&secret), - DecodingKey::from_secret(&secret), - )) +pub fn decode_token_rs256_secret(secret: &str) -> Result<(EncodingKey, DecodingKey)> { + let private_key = rsa::RsaPrivateKey::from_pkcs1_pem(secret).map_err(Error::RsaKeyError)?; + let public_key = private_key.to_public_key(); + let public_pkcs1_pem = public_key + .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF) + .map_err(Error::RsaKeyError)?; + + let encoding_key = EncodingKey::from_rsa_pem(secret.as_bytes()).map_err(Error::TokenError)?; + let decoding_key = + DecodingKey::from_rsa_pem(public_pkcs1_pem.as_bytes()).map_err(Error::TokenError)?; + + Ok((encoding_key, decoding_key)) } // bruh diff --git a/token/src/tests.rs b/token/src/tests.rs index 9a73210..c8bbb59 100644 --- a/token/src/tests.rs +++ b/token/src/tests.rs @@ -13,7 +13,7 @@ fn test_basic() { // "very secure secret" let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0"; - let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap(); + let dec_key = decode_token_rs256_secret(base64_secret).unwrap().1; /* {