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

server: HS256 -> RS256 secrets

This commit is contained in:
Graham Christensen 2023-11-06 15:56:39 -05:00 committed by Cole Helbling
parent 3e0b65a4c3
commit 9511afde4b
12 changed files with 188 additions and 48 deletions

133
Cargo.lock generated
View file

@ -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"

View file

@ -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.

View file

@ -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 ];
};

View file

@ -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 = ''
<option>services.atticd.credentialsFile</option> 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.
'';

View file

@ -101,7 +101,7 @@ pub async fn apply_auth<B>(req: Request<B>, next: Next<B>) -> Response {
.and_then(parse_authorization_header)
.and_then(|jwt| {
let state = req.extensions().get::<State>().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);
}

View file

@ -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);
}

View file

@ -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]

View file

@ -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)
}

View file

@ -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)?
};

View file

@ -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"

View file

@ -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<Self> {
// 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::<JWTClaims<TokenClaims>>(token, &key, &validation)
jsonwebtoken::decode::<JWTClaims<TokenClaims>>(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

View file

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