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;
/*
{