mirror of
https://github.com/zhaofengli/attic.git
synced 2024-12-14 11:57:30 +00:00
Merge pull request #177 from zhaofengli/rs256-support
Support RS256 JWTs
This commit is contained in:
commit
858120c450
14 changed files with 509 additions and 142 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -325,6 +325,7 @@ dependencies = [
|
|||
"maybe-owned",
|
||||
"rand",
|
||||
"regex",
|
||||
"rsa 0.9.6",
|
||||
"ryu",
|
||||
"sea-orm",
|
||||
"sea-orm-migration",
|
||||
|
@ -355,6 +356,7 @@ dependencies = [
|
|||
"jwt-simple",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"rsa 0.9.6",
|
||||
"serde",
|
||||
"serde_with",
|
||||
"tracing",
|
||||
|
|
|
@ -19,7 +19,7 @@ use crate::nix_store::StorePath;
|
|||
/// Expected values for `nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps`.
|
||||
pub const NO_DEPS: TestNar = TestNar {
|
||||
store_path: "/nix/store/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps",
|
||||
original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"),
|
||||
_original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"),
|
||||
nar: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.nar"),
|
||||
export: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.export"),
|
||||
closure: &["nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"],
|
||||
|
@ -31,7 +31,7 @@ pub const NO_DEPS: TestNar = TestNar {
|
|||
/// as `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
|
||||
pub const WITH_DEPS_A: TestNar = TestNar {
|
||||
store_path: "/nix/store/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a",
|
||||
original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"),
|
||||
_original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"),
|
||||
nar: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.nar"),
|
||||
export: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.export"),
|
||||
closure: &[
|
||||
|
@ -46,7 +46,7 @@ pub const WITH_DEPS_A: TestNar = TestNar {
|
|||
/// This depends on `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
|
||||
pub const WITH_DEPS_B: TestNar = TestNar {
|
||||
store_path: "/nix/store/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b",
|
||||
original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"),
|
||||
_original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"),
|
||||
nar: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.nar"),
|
||||
export: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.export"),
|
||||
closure: &[
|
||||
|
@ -58,7 +58,7 @@ pub const WITH_DEPS_B: TestNar = TestNar {
|
|||
/// Expected values for `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`.
|
||||
pub const WITH_DEPS_C: TestNar = TestNar {
|
||||
store_path: "/nix/store/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final",
|
||||
original_file: include_bytes!(
|
||||
_original_file: include_bytes!(
|
||||
"nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final"
|
||||
),
|
||||
nar: include_bytes!("nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final.nar"),
|
||||
|
@ -75,7 +75,7 @@ pub struct TestNar {
|
|||
store_path: &'static str,
|
||||
|
||||
/// The original file.
|
||||
original_file: &'static [u8],
|
||||
_original_file: &'static [u8],
|
||||
|
||||
/// A NAR dump without path metadata.
|
||||
nar: &'static [u8],
|
||||
|
|
|
@ -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
|
||||
nix run nixpkgs#openssl -- genrsa -traditional 4096 | base64 -w0
|
||||
```
|
||||
|
||||
Create a file on the server containing the following contents:
|
||||
|
||||
```
|
||||
ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from openssl"
|
||||
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="output from above"
|
||||
```
|
||||
|
||||
Ensure the file is only accessible by root.
|
||||
|
@ -47,6 +47,8 @@ You can import the module in one of two ways:
|
|||
settings = {
|
||||
listen = "[::]:8080";
|
||||
|
||||
jwt = { };
|
||||
|
||||
# Data chunking
|
||||
#
|
||||
# Warning: If you change any of the values here, it will be
|
||||
|
|
|
@ -5,8 +5,8 @@ let
|
|||
serverConfigFile = config.nodes.server.services.atticd.configFile;
|
||||
|
||||
cmd = {
|
||||
atticadm = "atticd-atticadm";
|
||||
atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticd -f ${serverConfigFile}";
|
||||
atticadm = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd-atticadm";
|
||||
atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd -f ${serverConfigFile}";
|
||||
};
|
||||
|
||||
makeTestDerivation = pkgs.writeShellScript "make-drv" ''
|
||||
|
@ -147,7 +147,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_BASE64='LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBekhqUzFGKzlRaFFUdlJZYjZ0UGhxS09FME5VYkIraTJMOTByWVBNQVVoYVBUMmlKCmVUNk9vWFlmZWszZlZ1dXIrYks1VWFVRjhUbEx2Y1FHa1Arckd0WDRiQUpGTWJBcTF3Y25FQ3R6ZGVERHJnSlIKMGUvNWJhdXQwSS9YS0ticG9oYjNvWVhtUmR5eG9WVGE3akY1bk11ajBsd25kUTcwYTF1ZGkzMGNpYkdTWHZMagpVeGltL3ByYjUrV3ZPdjN4UnhlbDZHYmptUW1RMVBHeHVLcmx3b1ZKRnlWTjl3QmExajBDelJDcURnTFRwQWw0CjhLVWlDY2V1VUZQcmdZaW9vSVhyVExlWmxVbFVVV3FHSDBJbGFKeVUyQ05iNWJtZWM1TnZ4RDlaakFoYytucmgKRS80VzkxajdQMFVyQnp4am9NUTRlKzBPZDhmQnBvSDAwbm4xUXdJREFRQUJBb0lCQUE2RmxEK21Ed3gyM1pJRAoxSGJBbHBuQ0IwaEhvbFJVK0Q5OC96d3k5ZlplaU00VWVCTUcyTjFweE1HTWIweStqeWU4UkVJaXJNSGRsbDRECllvNEF3bmUwODZCRUp3TG81cG4vOVl2RjhqelFla1ZNLzkrZm9nRGlmUVUvZWdIMm5NZzR4bHlQNUhOWXdicmEKQ25SNVNoQlRQQzdRQWJOa0hRTFU3bUwrUHowZUlXaG9KWVRoUUpkU0g3RDB0K1QwZzVVNDdPam5qbXJaTWwxaApHOE1IUHhKMk5WU1l2N0dobnpjblZvcVVxYzlxeldXRDZXZERtV1BPNGJ1K2p0b2E2U2o4cjJtb0RRZ1A5YXNhCm93RUFJbHBmbVkxYUx2dENwWG4rejRTTWJKcHRXMlVvaktGa2dkYm9jZmtXYWdtSGZRa2xmS0dBQ0hibU9ZV24KeDRCbTU3a0NnWUVBN1dXaXJDZnBRR01hR3A2WWxMQlVUc1VJSXJOclF4UmtuRlc3dFVYd0NqWFZ5SDlTR3FqNgphTkNhYzZpaks3QVNBYXlxY1JQRjFPY2gyNmxpVmRKUHNuRGxwUjhEVXB2TzRVOVRzSTJyZ1lZYzNrSWkzVGFKClgzV0Vic1Z6Nk45WXFPSXlnVnZiTEhLS0F4Uyt4b1Z2SjkzQmdWRHN5SkxRdmhrM3VubXk3M2tDZ1lFQTNINnYKeUhOKzllOVAyOS9zMVY1eWZxSjdvdVdKV0lBTHFDYm9zOTRRSVdPSG5HRUtSSGkydWIzR0d6U2tRSzN1eTUrdQo4M0txaFJOejRVMkdOK1pLaFE0NHhNVmV4TUVvZzJVU3lTaVZ0cFdqWXBwT2Q1NnVaMzRWaFU2TWRNZS9zT0JnCnNoei84MUxUSis2cHdFZE9wV2tPVlRaMXJISlZXQmdtVk5qWjc1c0NnWUVBNVd5YjBaU2dyMEVYTVRLa2NzNFcKTENudXV0cDZodEZtaWsrd29IZCtpOStMUThFSU1BdXVOUzJrbHJJYlAxVmhrWXkxQzZMNFJkRTV2M2ZyT05XUApmL3ZyYzdDTkhZREdacWlyVUswWldvdXB5b0pQLzBsOWFXdkJHT3hxSUZ2NDZ2M3ZvV1NNWkdBdFVOenpvaGZDClhOeks3WmF2dndka0JOT0tNQVQ5RU1FQ2dZRUF3NEhaWDRWNUo1d2dWVGVDQ2RjSzhsb2tBbFpBcUNZeEw5SUEKTjZ4STVUSVpSb0dNMXhXcC81dlRrci9rZkMwOU5YUExiclZYbVZPY1JrTzFKTStmZDhjYWN1OEdqck11dHdMaAoyMWVQR0N3cWlQMkZZZTlqZVFTRkZJU0hhZXpMZll3V2NSZmhvdURudGRxYXpaRHNuU0kvd1RMZXVCOVFxU0lRCnF0NzByczBDZ1lCQ2lzV0VKdXpQUUlJNzVTVkU4UnJFZGtUeUdhOEVBOHltcStMdDVLRDhPYk80Q2JHYVFlWXkKWFpjSHVyOFg2cW1lWHZVU3MwMHBMMUdnTlJ3WCtSUjNMVDhXTm9vc0NqVDlEUW9GOFZveEtseDROVTRoUGlrTQpBc0w1RS9wYnVLeXkvSU5LTnQyT3ZPZmJYVitlTXZQdGs5c1dORjNyRTBYcU15TW9maG9NaVE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo='
|
||||
'';
|
||||
|
||||
services.atticd = {
|
||||
|
@ -156,6 +156,8 @@ in {
|
|||
settings = {
|
||||
listen = "[::]:8080";
|
||||
|
||||
jwt = { };
|
||||
|
||||
chunking = {
|
||||
nar-size-threshold = 1;
|
||||
min-size = 64 * 1024;
|
||||
|
@ -165,7 +167,7 @@ in {
|
|||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.attic-server ];
|
||||
environment.systemPackages = [ pkgs.openssl pkgs.attic-server ];
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 8080 ];
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ let
|
|||
} ''
|
||||
cat $configFile
|
||||
|
||||
export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ="
|
||||
export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${pkgs.openssl}/bin/openssl genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)"
|
||||
export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
|
||||
${cfg.package}/bin/atticd --mode check-config -f $configFile
|
||||
cat <$configFile >$out
|
||||
|
@ -79,8 +79,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_BASE64: The base64-encoded RSA PEM PKCS1 of the
|
||||
RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`.
|
||||
'';
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
|
@ -154,9 +154,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 | base64 -w0` 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.
|
||||
'';
|
||||
|
|
|
@ -60,6 +60,7 @@ tracing-subscriber = { version = "0.3.17", features = [ "json" ] }
|
|||
uuid = { version = "1.3.3", features = ["v4"] }
|
||||
console-subscriber = "0.2.0"
|
||||
xdg = "2.5.0"
|
||||
rsa = "0.9.3"
|
||||
|
||||
[dependencies.async-compression]
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! HTTP middlewares for access control.
|
||||
|
||||
use attic::cache::CacheName;
|
||||
use attic_token::util::parse_authorization_header;
|
||||
use axum::{extract::Request, middleware::Next, response::Response};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tokio::sync::OnceCell;
|
||||
|
@ -8,8 +10,6 @@ use crate::access::{CachePermission, Token};
|
|||
use crate::database::{entity::cache::CacheModel, AtticDatabase};
|
||||
use crate::error::ServerResult;
|
||||
use crate::{RequestState, State};
|
||||
use attic::cache::CacheName;
|
||||
use attic_token::util::parse_authorization_header;
|
||||
|
||||
/// Auth state.
|
||||
#[derive(Debug)]
|
||||
|
@ -101,10 +101,19 @@ pub async fn apply_auth(req: Request, next: Next) -> 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);
|
||||
let signature_type = state.config.jwt.signing_config.clone().into();
|
||||
|
||||
let res_token = Token::from_jwt(
|
||||
&jwt,
|
||||
&signature_type,
|
||||
&state.config.jwt.token_bound_issuer,
|
||||
&state.config.jwt.token_bound_audiences,
|
||||
);
|
||||
|
||||
if let Err(e) = &res_token {
|
||||
tracing::debug!("Ignoring bad JWT token: {}", e);
|
||||
}
|
||||
|
||||
res_token.ok()
|
||||
});
|
||||
|
||||
|
|
|
@ -115,7 +115,13 @@ 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)?;
|
||||
let signature_type = config.jwt.signing_config.into();
|
||||
|
||||
let encoded_token = token.encode(
|
||||
&signature_type,
|
||||
&config.jwt.token_bound_issuer,
|
||||
&config.jwt.token_bound_audiences,
|
||||
)?;
|
||||
println!("{}", encoded_token);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,13 +34,6 @@ allowed-hosts = []
|
|||
# cache.
|
||||
#require-proof-of-possession = true
|
||||
|
||||
# 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
|
||||
# variable.
|
||||
token-hs256-secret-base64 = "%token_hs256_secret_base64%"
|
||||
|
||||
# Database connection
|
||||
[database]
|
||||
# Connection URL
|
||||
|
@ -85,7 +78,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
|
||||
|
@ -134,3 +127,37 @@ interval = "12 hours"
|
|||
# Zero (default) means time-based garbage-collection is
|
||||
# disabled by default. You can enable it on a per-cache basis.
|
||||
#default-retention-period = "6 months"
|
||||
|
||||
[jwt]
|
||||
# WARNING: Changing _anything_ in this section will break any existing
|
||||
# tokens. If you need to regenerate them, ensure that you use the the
|
||||
# correct secret and include the `iss` and `aud` claims.
|
||||
|
||||
# JWT `iss` claim
|
||||
#
|
||||
# Set this to the JWT issuer that you want to validate.
|
||||
# If this is set, all received JWTs will validate that the `iss` claim
|
||||
# matches this value.
|
||||
#token-bound-issuer = "some-issuer"
|
||||
|
||||
# JWT `aud` claim
|
||||
#
|
||||
# Set this to the JWT audience(s) that you want to validate.
|
||||
# If this is set, all received JWTs will validate that the `aud` claim
|
||||
# contains at least one of these values.
|
||||
#token-bound-audiences = ["some-audience1", "some-audience2"]
|
||||
|
||||
[jwt.signing]
|
||||
# JWT RS256 secret key
|
||||
#
|
||||
# Set this to the base64-encoded private half of an RSA PEM PKCS1 key.
|
||||
# You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64`
|
||||
# environment variable.
|
||||
token-rs256-secret-base64 = "%token_rs256_secret_base64%"
|
||||
|
||||
# JWT HS256 secret key
|
||||
#
|
||||
# Set this to the base64-encoded HMAC secret key.
|
||||
# You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64`
|
||||
# environment variable.
|
||||
#token-hs256-secret-base64 = ""
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Server configuration.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -7,12 +8,16 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::Result;
|
||||
use async_compression::Level as CompressionLevel;
|
||||
use attic_token::SignatureType;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use derivative::Derivative;
|
||||
use serde::{de, Deserialize};
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::access::{decode_token_hs256_secret_base64, HS256Key};
|
||||
use crate::access::{
|
||||
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};
|
||||
|
||||
|
@ -26,9 +31,18 @@ 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.
|
||||
/// Environment variable storing the base64-encoded HMAC secret (used for signing and verifying
|
||||
/// received JWTs).
|
||||
const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64";
|
||||
|
||||
/// Environment variable storing the base64-encoded RSA PEM PKCS1 private key (used for signing and
|
||||
/// 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";
|
||||
|
||||
|
@ -108,14 +122,81 @@ pub struct Config {
|
|||
#[serde(default = "Default::default")]
|
||||
pub garbage_collection: GarbageCollectionConfig,
|
||||
|
||||
/// JSON Web Token.
|
||||
pub jwt: JWTConfig,
|
||||
|
||||
/// (Deprecated Stub)
|
||||
///
|
||||
/// This simply results in an error telling the user to update
|
||||
/// their configuration.
|
||||
#[serde(rename = "token-hs256-secret-base64")]
|
||||
#[serde(default = "Default::default")]
|
||||
#[serde(deserialize_with = "deserialize_deprecated_token_hs256_secret")]
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub _depreated_token_hs256_secret: Option<String>,
|
||||
}
|
||||
|
||||
/// JSON Web Token configuration.
|
||||
#[derive(Clone, Derivative, Deserialize)]
|
||||
#[derivative(Debug)]
|
||||
pub struct JWTConfig {
|
||||
/// The `iss` claim of the JWT.
|
||||
///
|
||||
/// If specified, received JWTs must have this claim, and its value must match this
|
||||
/// configuration.
|
||||
#[serde(rename = "token-bound-issuer")]
|
||||
#[serde(default = "Default::default")]
|
||||
pub token_bound_issuer: Option<String>,
|
||||
|
||||
/// The `aud` claim of the JWT.
|
||||
///
|
||||
/// If specified, received JWTs must have this claim, and must contain one of the configured
|
||||
/// values.
|
||||
#[serde(rename = "token-bound-audiences")]
|
||||
#[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 RSA pubkey.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Set this to the base64-encoded RSA PEM PKCS1 private key to use for signing and verifying
|
||||
/// JWTs.
|
||||
#[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 encoding of a randomly generated 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")]
|
||||
#[serde(default = "load_token_hs256_secret_from_env")]
|
||||
#[derivative(Debug = "ignore")]
|
||||
pub token_hs256_secret: HS256Key,
|
||||
HS256SignAndVerify(HS256Key),
|
||||
}
|
||||
|
||||
impl From<JWTSigningConfig> for SignatureType {
|
||||
fn from(value: JWTSigningConfig) -> Self {
|
||||
match value {
|
||||
JWTSigningConfig::RS256VerifyOnly(key) => Self::RS256PubkeyOnly(key),
|
||||
JWTSigningConfig::RS256SignAndVerify(key) => Self::RS256(key),
|
||||
JWTSigningConfig::HS256SignAndVerify(key) => Self::HS256(key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Database connection configuration.
|
||||
|
@ -240,16 +321,82 @@ pub struct GarbageCollectionConfig {
|
|||
pub default_retention_period: Duration,
|
||||
}
|
||||
|
||||
fn load_token_hs256_secret_from_env() -> HS256Key {
|
||||
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_jwt_signing_config_from_env() -> JWTSigningConfig {
|
||||
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 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\
|
||||
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\
|
||||
"
|
||||
)
|
||||
};
|
||||
|
||||
decode_token_hs256_secret_base64(&s).expect("Failed to load as decoding key")
|
||||
config
|
||||
}
|
||||
|
||||
fn load_token_hs256_secret_from_env() -> Option<JWTSigningConfig> {
|
||||
let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64).ok()?;
|
||||
|
||||
decode_token_hs256_secret_base64(&s)
|
||||
.ok()
|
||||
.map(JWTSigningConfig::HS256SignAndVerify)
|
||||
}
|
||||
|
||||
fn load_token_rs256_secret_from_env() -> Option<JWTSigningConfig> {
|
||||
let s = env::var(ENV_TOKEN_RS256_SECRET_BASE64).ok()?;
|
||||
|
||||
decode_token_rs256_secret_base64(&s)
|
||||
.ok()
|
||||
.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("Database URL must be specified in either database.url or the ATTIC_SERVER_DATABASE_URL environment.")
|
||||
env::var(ENV_DATABASE_URL).expect(&format!(
|
||||
"Database URL must be specified in either database.url \
|
||||
or the {ENV_DATABASE_URL} environment."
|
||||
))
|
||||
}
|
||||
|
||||
impl CompressionConfig {
|
||||
|
@ -296,6 +443,30 @@ impl Default for GarbageCollectionConfig {
|
|||
}
|
||||
}
|
||||
|
||||
fn deserialize_deprecated_token_hs256_secret<'de, D>(
|
||||
_deserializer: D,
|
||||
) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
use de::Error;
|
||||
|
||||
Err(Error::custom(
|
||||
"\n\
|
||||
The token-hs256-secret-base64 field has been moved to [jwt.signing].\n\
|
||||
\n\
|
||||
To continue using HS256 signing, move your current config:\n\
|
||||
\n\
|
||||
token-hs256-secret-base64 = \"your token\"\n\
|
||||
\n\
|
||||
To the bottom of the file like so:\n\
|
||||
\n\
|
||||
[jwt.signing]\n\
|
||||
token-hs256-secret-base64 = \"your token\"\n\
|
||||
",
|
||||
))
|
||||
}
|
||||
|
||||
fn deserialize_token_hs256_secret_base64<'de, D>(deserializer: D) -> Result<HS256Key, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
|
@ -308,6 +479,32 @@ where
|
|||
Ok(key)
|
||||
}
|
||||
|
||||
fn deserialize_token_rs256_secret_base64<'de, D>(deserializer: D) -> Result<RS256KeyPair, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
use de::Error;
|
||||
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let key = decode_token_rs256_secret_base64(&s).map_err(Error::custom)?;
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -14,11 +14,10 @@
|
|||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use chrono::{Months, Utc};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::Rng;
|
||||
use rsa::pkcs1::EncodeRsaPrivateKey;
|
||||
use tokio::fs::{self, OpenOptions};
|
||||
|
||||
use crate::access::{decode_token_hs256_secret_base64, Token};
|
||||
use crate::access::{decode_token_rs256_secret_base64, SignatureType, Token};
|
||||
use crate::config;
|
||||
use attic::cache::CacheNamePattern;
|
||||
|
||||
|
@ -45,20 +44,18 @@ 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 random: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(128)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let rs256_secret_base64 = {
|
||||
let mut rng = rand::thread_rng();
|
||||
let private_key = rsa::RsaPrivateKey::new(&mut rng, 4096)?;
|
||||
let pkcs1_pem = private_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)?;
|
||||
|
||||
BASE64_STANDARD.encode(random)
|
||||
BASE64_STANDARD.encode(pkcs1_pem.as_bytes())
|
||||
};
|
||||
|
||||
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_base64%", &rs256_secret_base64);
|
||||
|
||||
fs::write(&config_path, config_content.as_bytes()).await?;
|
||||
|
||||
|
@ -76,8 +73,8 @@ 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();
|
||||
token.encode(&key)?
|
||||
let key = decode_token_rs256_secret_base64(&rs256_secret_base64).unwrap();
|
||||
token.encode(&SignatureType::RS256(key), &None, &None)?
|
||||
};
|
||||
|
||||
eprintln!();
|
||||
|
|
|
@ -9,7 +9,7 @@ edition = "2021"
|
|||
attic = { path = "../attic", default-features = false }
|
||||
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.24"
|
||||
chrono = "0.4.31"
|
||||
displaydoc = "0.2.4"
|
||||
indexmap = { version = "2.2.6", features = ["serde"] }
|
||||
jwt-simple = "0.11.5"
|
||||
|
@ -18,3 +18,4 @@ regex = "1.8.3"
|
|||
serde = "1.0.163"
|
||||
serde_with = "3.0.0"
|
||||
tracing = "0.1.37"
|
||||
rsa = "0.9.3"
|
||||
|
|
135
token/src/lib.rs
135
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 the configured 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
|
||||
|
@ -83,14 +83,16 @@ pub mod util;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use chrono::{DateTime, Utc};
|
||||
use displaydoc::Display;
|
||||
use indexmap::IndexMap;
|
||||
use jwt_simple::prelude::{Duration, RSAKeyPairLike, RSAPublicKeyLike, VerificationOptions};
|
||||
pub use jwt_simple::{
|
||||
algorithms::{HS256Key, MACLike},
|
||||
algorithms::{HS256Key, MACLike, RS256KeyPair, RS256PublicKey},
|
||||
claims::{Claims, JWTClaims},
|
||||
prelude::UnixTimeStamp,
|
||||
};
|
||||
|
@ -155,49 +157,49 @@ pub struct AtticAccess {
|
|||
pub struct CachePermission {
|
||||
/// Can pull objects from the cache.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "r")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub pull: bool,
|
||||
|
||||
/// Can push objects to the cache.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "w")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub push: bool,
|
||||
|
||||
/// Can delete objects from the cache.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "d")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub delete: bool,
|
||||
|
||||
/// Can create the cache itself.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "cc")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub create_cache: bool,
|
||||
|
||||
/// Can reconfigure the cache.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "cr")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub configure_cache: bool,
|
||||
|
||||
/// Can configure retention/quota settings.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "cq")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub configure_cache_retention: bool,
|
||||
|
||||
/// Can destroy the cache itself.
|
||||
#[serde(default = "CachePermission::permission_default")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
#[serde(rename = "cd")]
|
||||
#[serde_as(as = "BoolFromInt")]
|
||||
pub destroy_cache: bool,
|
||||
|
@ -223,14 +225,68 @@ pub enum Error {
|
|||
|
||||
/// Base64 decode error: {0}
|
||||
Base64Error(base64::DecodeError),
|
||||
|
||||
/// RSA Key error: {0}
|
||||
RsaKeyError(rsa::pkcs1::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 {
|
||||
/// Verifies and decodes a token.
|
||||
pub fn from_jwt(token: &str, key: &HS256Key) -> Result<Self> {
|
||||
key.verify_token(token, None)
|
||||
.map_err(Error::TokenError)
|
||||
.map(Token)
|
||||
pub fn from_jwt(
|
||||
token: &str,
|
||||
signature_type: &SignatureType,
|
||||
maybe_bound_issuer: &Option<String>,
|
||||
maybe_bound_audiences: &Option<HashSet<String>>,
|
||||
) -> Result<Self> {
|
||||
let opts = VerificationOptions {
|
||||
reject_before: None,
|
||||
accept_future: false,
|
||||
required_subject: None,
|
||||
required_key_id: None,
|
||||
required_public_key: None,
|
||||
required_nonce: None,
|
||||
allowed_issuers: maybe_bound_issuer
|
||||
.as_ref()
|
||||
.map(|s| [s.to_owned()].into())
|
||||
.to_owned(),
|
||||
allowed_audiences: maybe_bound_audiences.to_owned(),
|
||||
time_tolerance: None,
|
||||
max_validity: None,
|
||||
max_token_length: None,
|
||||
max_header_length: None,
|
||||
artificial_time: None,
|
||||
};
|
||||
|
||||
match signature_type {
|
||||
SignatureType::HS256(key) => key
|
||||
.verify_token(token, Some(opts))
|
||||
.map_err(Error::TokenError)
|
||||
.map(Token),
|
||||
SignatureType::RS256(key) => {
|
||||
let public_key = key.public_key();
|
||||
public_key
|
||||
.verify_token(token, Some(opts))
|
||||
.map_err(Error::TokenError)
|
||||
.map(Token)
|
||||
}
|
||||
SignatureType::RS256PubkeyOnly(key) => key
|
||||
.verify_token(token, Some(opts))
|
||||
.map_err(Error::TokenError)
|
||||
.map(Token),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new token with an expiration timestamp.
|
||||
|
@ -239,12 +295,17 @@ impl Token {
|
|||
attic_ns: Default::default(),
|
||||
};
|
||||
|
||||
let now_epoch = Utc::now().signed_duration_since(DateTime::UNIX_EPOCH);
|
||||
|
||||
Self(JWTClaims {
|
||||
issued_at: None,
|
||||
expires_at: Some(UnixTimeStamp::from_secs(
|
||||
exp.timestamp().try_into().unwrap(),
|
||||
)),
|
||||
invalid_before: None,
|
||||
invalid_before: Some(Duration::new(
|
||||
now_epoch.num_seconds().try_into().unwrap(),
|
||||
0,
|
||||
)),
|
||||
issuer: None,
|
||||
subject: Some(sub),
|
||||
audiences: None,
|
||||
|
@ -255,8 +316,28 @@ impl Token {
|
|||
}
|
||||
|
||||
/// Encodes the token.
|
||||
pub fn encode(&self, key: &HS256Key) -> Result<String> {
|
||||
key.authenticate(self.0.clone()).map_err(Error::TokenError)
|
||||
pub fn encode(
|
||||
&self,
|
||||
signature_type: &SignatureType,
|
||||
maybe_bound_issuer: &Option<String>,
|
||||
maybe_bound_audiences: &Option<HashSet<String>>,
|
||||
) -> Result<String> {
|
||||
let mut token = self.0.clone();
|
||||
|
||||
if let Some(issuer) = maybe_bound_issuer {
|
||||
token = token.with_issuer(issuer);
|
||||
}
|
||||
if let Some(audiences) = maybe_bound_audiences {
|
||||
token = token.with_audiences(audiences.to_owned());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the subject of the token.
|
||||
|
@ -362,11 +443,23 @@ impl CachePermission {
|
|||
impl StdError for Error {}
|
||||
|
||||
pub fn decode_token_hs256_secret_base64(s: &str) -> Result<HS256Key> {
|
||||
let secret = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
|
||||
Ok(HS256Key::from_bytes(&secret))
|
||||
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
|
||||
let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
|
||||
Ok(HS256Key::from_bytes(&secret.as_bytes()))
|
||||
}
|
||||
|
||||
// bruh
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
pub fn decode_token_rs256_secret_base64(s: &str) -> Result<RS256KeyPair> {
|
||||
let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?;
|
||||
let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?;
|
||||
let keypair = RS256KeyPair::from_pem(secret).map_err(Error::TokenError)?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -10,15 +10,12 @@ macro_rules! cache {
|
|||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
// "very secure secret"
|
||||
let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0";
|
||||
|
||||
let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap();
|
||||
|
||||
/*
|
||||
$ cat json
|
||||
{
|
||||
"sub": "meow",
|
||||
"exp": 4102324986,
|
||||
"nbf": 0,
|
||||
"https://jwt.attic.rs/v1": {
|
||||
"caches": {
|
||||
"all-*": {"r":1},
|
||||
|
@ -31,69 +28,102 @@ fn test_basic() {
|
|||
}
|
||||
*/
|
||||
|
||||
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw";
|
||||
let tokens: &[(&str, Box<dyn Fn() -> Token>)] = &[
|
||||
(
|
||||
"hs256",
|
||||
Box::new(|| {
|
||||
// "very secure secret"
|
||||
let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0";
|
||||
let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap();
|
||||
|
||||
// NOTE(cole-h): check that we get a consistent iteration order when getting permissions for
|
||||
// caches -- this depends on the order of the fields in the token, but should otherwise be
|
||||
// consistent between iterations
|
||||
let mut was_ever_wrong = false;
|
||||
for _ in 0..=1_000 {
|
||||
// NOTE(cole-h): we construct a new Token every iteration in order to get different "random
|
||||
// state"
|
||||
let decoded = Token::from_jwt(token, &dec_key).unwrap();
|
||||
let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" });
|
||||
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw";
|
||||
|
||||
// NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be
|
||||
// retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though
|
||||
// the `all-*` pattern (which only allows reading/pulling) is specified first
|
||||
if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() {
|
||||
was_ever_wrong = true;
|
||||
Token::from_jwt(token, &SignatureType::HS256(dec_key), &None, &None).unwrap()
|
||||
}),
|
||||
),
|
||||
(
|
||||
"rs256",
|
||||
Box::new(|| {
|
||||
// nix shell nixpkgs#jwt-cli
|
||||
// openssl genpkey -out rs256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform der
|
||||
// BASE64_SECRET=$(openssl rsa -in rs256 -outform PEM -traditional | base64 -w0)
|
||||
let base64_secret = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNUZranRMRzV5eS9pMFlnYkQxeUJBK21GckNmLzZiQ2F0TDFFQ3ppNG1tZWhSZTcwCkFEL0dSSHhTVUErc0pZeCtZNjlyL0RqQWs2OFJlQ1c4b2FQWXhtc21RNG5VM2ZwZ2E3WWFqZ3ZoWmVsa3JtaC8KZ1ZURWtFTG1IZlJtQkwvOWlsT20yRHNtYTVhUFo0SFl6ellpdjJvcFF5UGRndXcyWXFtbzE3Nk5MdllCMmpJTwovR3FkdE55K3NPV296NktVSVlJa0hWWU5HMENVcFNzdXBqUTJ6VTVZMFc2UXlNQWFWd1BONElJT3lXWUNwZXRECjFJbWxYekhROXM4NXFSWnlLa21iZFhtTVBVWmUvekRxc2FFd3lscFlpT0RjbDdRYU5QTzEzZnk3UGtQMmVwdUkKTk5tZ1E0WEF0MkF4ZXNKck5ibUs4aG1iM3doRXZkNjRFMGdEV1FJREFRQUJBb0lCQUJEemNRd2IyVi8wK1JCMgoyeE5qMll2eHpPTi93S2FYWHBTbUxDUHRIUDhSVEU2RnM0VkZOckdrelBOMmhsL3ZNdjZ4YWdHNk1NbUZ5SFV6CnovSHIyTTY1NjRnOTloaFlXc29FSmFwL3hVYXNjYlhrdWZwZTBZeW4rcThra21JdDRtTmZYRlpXNWI0ODJmNWsKRERVdG5weTVBOEVoSzNOcGw0dnhia0E5dS90TlVlT1NHTkhPYVZjcHdERVhDNXJ4bmFxTm5wMkMwa1A4ODRINgpSb2lZVkF4bytHaVpNVzhIOFRmSXVsenh3c04yQnVNcUNmOGVhNG1EM0pRVHZ2REhhUHM4eVJTUlB3UmlHYUkzCnVybFRmdjg4U20va09oL0N2SkpoRnhCVkVNVjIydWRNUmU3L3NpTWtlbVlvUnhaTWJjRGVQK2h1RktJWTRSMEoKNnRJUHQ3VUNnWUVBOTlhL2IzeFBsQWh0ck02dUlUUXNQd0FYQUg3Q1NXL1FSdVJUTWVhYXVIMk9sRitjZmpMNApJS1Nsdy9QaUtaUEk1TFRWM2ZVZk5WNTVsOFZHTytsT2ViTFhnaXBYM3BqSDBma3AyY3Q2Smk3aGw0aUlXK0h0ClpJNE9KYkYwTTBETHdySkd3T25QL2trRHNxSW9IbC9MdTBRM2FxSm1RVCsvcG54R083R21kbDhDZ1lFQTY5NFcKZHF2NnF4VjF5V0Z4QWZOOE1hZStpTC9xY1VhTm85ZzMva2YvOXZ3VXdtcERvR0xnaVVLMWZKb3BUYlBjcWgwRwptbUZEQ3V2M1Q0OS9yU2k5dU4zYm82cmlXRUl4VFg1YUtFSjlpSEFMWDJGWDdGSDJRdUZGWEwzQ2c0ckdvL1pDCmdjUkxuS3dma3JUVnRxeEdaNjN4YmsvcFpHWjZtTW01VkNDck1VY0NnWUVBc3JUT1pQMG1CSC92VldQU2UyNjcKV05JZncrT2pCSUR6bGFxZHNxV3Rlc3BPUFA2VVFRdFBqM29wYlJvMlFmU21Md09XRXUzbEN2Nk1mcnRvNFZwaAprNjg1WmtwU0FkZjRmWmRFYmg4aWZOWGhKUHIyR0FyWXVtRVVJbW5LZUFxSTRtTGFVZEJHZ2Z6MEJhS1hldzlvClFDZjRMWlBjVjhBMzJUeFRDRWdZMTlFQ2dZQU04U2F5WkVWZzFkQ2N1Q2dIUDJEMUtJc2YzY2Z6WnplbVlkclEKclFxeWRxcDg4Rys5Z1M5bzJLdzBwaERXSHFSaEFTNjNrZGFuNXNLdkx1U0dqOUc1THhNNks4bzNwWW9uQW1QWQpDYTN4cXBRMUs1WXpkVnZaMTVxQ3VEYlFHUEZGVmVIWVZQa0JJOENud0J4cDVaSUhabGYxQVpXQTJNNnBTNGhMCndXOGpTUUtCZ1FDQmNJbjU4Y0lmZkhmMjM4SUJvZnR1UVVzREZGcnkzaUVpaWpTYmJ1WnB1Vm8zL2pWbUsyaEYKS2xUL2xoRDdWdGJ1V3phMG9WQmZDaWZqMnZ2S2pmZ0l6NnF3Um1UbC9DSjlWdUNHTUI1VG55cGl3OEtodXorSAo0L2twdDdNcW9WQ0dRSjd1WVQyQzY1K0JqNklnUnBQT09za3VKNW1RZ0FlbTQ3eDBrVnRSemc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=";
|
||||
|
||||
let dec_key = decode_token_rs256_secret_base64(base64_secret).unwrap();
|
||||
|
||||
// TOKEN=$(jq -c < json | jwt encode --alg RS256 --secret @./rs256 -)
|
||||
let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MjIwMDUwNzksIm5iZiI6MCwic3ViIjoibWVvdyJ9.Zs24IUbQOpOjhEe0sfsoSSJhDrzf4v-_wX_ceKqHeb2MERY8XSIQ1RPTNVeOW4LfJHumJj_rxh8Wv2BRGZSMldrTt0Ab_N7FnkhA37_jnRvgvEjSG3V4fC8aA4KoOa-43NRpg4HmPxiXte5-6LneBOR94Wss868wC1b_2yX2zCc1wQoZA3LNo-CRLnL4Yp5wY4Bbgyguv_9mfqXVYZykZnxumyGwVFD-Rub3KQ9d53Rf9tKcvRk9qxO2q8F2PKjeaUBG2xZtGwkWTMvSmwR1dKtkPUyPggOzbLoUG-6fxfo7D3NyL5qWCSN_7CkI-xlsRSLY1gTq-FqXvcpHeZbc8w";
|
||||
|
||||
Token::from_jwt(token, &SignatureType::RS256(dec_key), &None, &None).unwrap()
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, decode) in tokens {
|
||||
eprintln!("Testing {name}");
|
||||
|
||||
// NOTE(cole-h): check that we get a consistent iteration order when getting permissions for
|
||||
// caches -- this depends on the order of the fields in the token, but should otherwise be
|
||||
// consistent between iterations
|
||||
let mut was_ever_wrong = false;
|
||||
for _ in 0..=1_000 {
|
||||
// NOTE(cole-h): we construct a new Token every iteration in order to get different "random
|
||||
// state"
|
||||
let decoded = decode();
|
||||
let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" });
|
||||
|
||||
// NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be
|
||||
// retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though
|
||||
// the `all-*` pattern (which only allows reading/pulling) is specified first
|
||||
if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() {
|
||||
was_ever_wrong = true;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
!was_ever_wrong,
|
||||
"Iteration order should be consistent to prevent random auth failures (and successes)"
|
||||
);
|
||||
|
||||
let decoded = decode();
|
||||
|
||||
let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" });
|
||||
|
||||
assert!(perm_rw.pull);
|
||||
assert!(perm_rw.push);
|
||||
assert!(!perm_rw.delete);
|
||||
assert!(!perm_rw.create_cache);
|
||||
|
||||
assert!(perm_rw.require_pull().is_ok());
|
||||
assert!(perm_rw.require_push().is_ok());
|
||||
assert!(perm_rw.require_delete().is_err());
|
||||
assert!(perm_rw.require_create_cache().is_err());
|
||||
|
||||
let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" });
|
||||
|
||||
assert!(perm_ro.pull);
|
||||
assert!(!perm_ro.push);
|
||||
assert!(!perm_ro.delete);
|
||||
assert!(!perm_ro.create_cache);
|
||||
|
||||
assert!(perm_ro.require_pull().is_ok());
|
||||
assert!(perm_ro.require_push().is_err());
|
||||
assert!(perm_ro.require_delete().is_err());
|
||||
assert!(perm_ro.require_create_cache().is_err());
|
||||
|
||||
let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" });
|
||||
|
||||
assert!(perm_team.pull);
|
||||
assert!(perm_team.push);
|
||||
assert!(!perm_team.delete);
|
||||
assert!(perm_team.create_cache);
|
||||
|
||||
assert!(perm_team.require_pull().is_ok());
|
||||
assert!(perm_team.require_push().is_ok());
|
||||
assert!(perm_team.require_delete().is_err());
|
||||
assert!(perm_team.require_create_cache().is_ok());
|
||||
|
||||
assert!(!decoded
|
||||
.get_permission_for_cache(&cache! { "forbidden-cache" })
|
||||
.can_discover());
|
||||
}
|
||||
assert!(
|
||||
!was_ever_wrong,
|
||||
"Iteration order should be consistent to prevent random auth failures (and successes)"
|
||||
);
|
||||
|
||||
let decoded = Token::from_jwt(token, &dec_key).unwrap();
|
||||
|
||||
let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" });
|
||||
|
||||
assert!(perm_rw.pull);
|
||||
assert!(perm_rw.push);
|
||||
assert!(!perm_rw.delete);
|
||||
assert!(!perm_rw.create_cache);
|
||||
|
||||
assert!(perm_rw.require_pull().is_ok());
|
||||
assert!(perm_rw.require_push().is_ok());
|
||||
assert!(perm_rw.require_delete().is_err());
|
||||
assert!(perm_rw.require_create_cache().is_err());
|
||||
|
||||
let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" });
|
||||
|
||||
assert!(perm_ro.pull);
|
||||
assert!(!perm_ro.push);
|
||||
assert!(!perm_ro.delete);
|
||||
assert!(!perm_ro.create_cache);
|
||||
|
||||
assert!(perm_ro.require_pull().is_ok());
|
||||
assert!(perm_ro.require_push().is_err());
|
||||
assert!(perm_ro.require_delete().is_err());
|
||||
assert!(perm_ro.require_create_cache().is_err());
|
||||
|
||||
let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" });
|
||||
|
||||
assert!(perm_team.pull);
|
||||
assert!(perm_team.push);
|
||||
assert!(!perm_team.delete);
|
||||
assert!(perm_team.create_cache);
|
||||
|
||||
assert!(perm_team.require_pull().is_ok());
|
||||
assert!(perm_team.require_push().is_ok());
|
||||
assert!(perm_team.require_delete().is_err());
|
||||
assert!(perm_team.require_create_cache().is_ok());
|
||||
|
||||
assert!(!decoded
|
||||
.get_permission_for_cache(&cache! { "forbidden-cache" })
|
||||
.can_discover());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue