A fork of attic a self-hostable Nix Binary Cache server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #177 from zhaofengli/rs256-support

Support RS256 JWTs

authored by

Zhaofeng Li and committed by
GitHub
858120c4 61ebdef2

+499 -132
+2
Cargo.lock
··· 325 325 "maybe-owned", 326 326 "rand", 327 327 "regex", 328 + "rsa 0.9.6", 328 329 "ryu", 329 330 "sea-orm", 330 331 "sea-orm-migration", ··· 355 356 "jwt-simple", 356 357 "lazy_static", 357 358 "regex", 359 + "rsa 0.9.6", 358 360 "serde", 359 361 "serde_with", 360 362 "tracing",
+5 -5
attic/src/nix_store/tests/test_nar.rs
··· 19 19 /// Expected values for `nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps`. 20 20 pub const NO_DEPS: TestNar = TestNar { 21 21 store_path: "/nix/store/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps", 22 - original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"), 22 + _original_file: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"), 23 23 nar: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.nar"), 24 24 export: include_bytes!("nar/nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps.export"), 25 25 closure: &["nm1w9sdm6j6icmhd2q3260hl1w9zj6li-attic-test-no-deps"], ··· 31 31 /// as `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`. 32 32 pub const WITH_DEPS_A: TestNar = TestNar { 33 33 store_path: "/nix/store/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a", 34 - original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"), 34 + _original_file: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a"), 35 35 nar: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.nar"), 36 36 export: include_bytes!("nar/n7q4i7rlmbk4xz8qdsxpm6jbhrnxraq2-attic-test-with-deps-a.export"), 37 37 closure: &[ ··· 46 46 /// This depends on `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`. 47 47 pub const WITH_DEPS_B: TestNar = TestNar { 48 48 store_path: "/nix/store/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b", 49 - original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"), 49 + _original_file: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b"), 50 50 nar: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.nar"), 51 51 export: include_bytes!("nar/544qcchwgcgpz3xi1bbml28f8jj6009p-attic-test-with-deps-b.export"), 52 52 closure: &[ ··· 58 58 /// Expected values for `3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final`. 59 59 pub const WITH_DEPS_C: TestNar = TestNar { 60 60 store_path: "/nix/store/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final", 61 - original_file: include_bytes!( 61 + _original_file: include_bytes!( 62 62 "nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final" 63 63 ), 64 64 nar: include_bytes!("nar/3k1wymic8p7h5pfcqfhh0jan8ny2a712-attic-test-with-deps-c-final.nar"), ··· 75 75 store_path: &'static str, 76 76 77 77 /// The original file. 78 - original_file: &'static [u8], 78 + _original_file: &'static [u8], 79 79 80 80 /// A NAR dump without path metadata. 81 81 nar: &'static [u8],
+5 -3
book/src/admin-guide/deployment/nixos.md
··· 11 11 12 12 ## Generating the Credentials File 13 13 14 - The HS256 JWT secret can be generated with the `openssl` utility: 14 + The RS256 JWT secret can be generated with the `openssl` utility: 15 15 16 16 ```bash 17 - openssl rand 64 | base64 -w0 17 + nix run nixpkgs#openssl -- genrsa -traditional 4096 | base64 -w0 18 18 ``` 19 19 20 20 Create a file on the server containing the following contents: 21 21 22 22 ``` 23 - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from openssl" 23 + ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="output from above" 24 24 ``` 25 25 26 26 Ensure the file is only accessible by root. ··· 46 46 47 47 settings = { 48 48 listen = "[::]:8080"; 49 + 50 + jwt = { }; 49 51 50 52 # Data chunking 51 53 #
+6 -4
integration-tests/basic/default.nix
··· 5 5 serverConfigFile = config.nodes.server.services.atticd.configFile; 6 6 7 7 cmd = { 8 - atticadm = "atticd-atticadm"; 9 - atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 && atticd -f ${serverConfigFile}"; 8 + atticadm = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd-atticadm"; 9 + atticd = ". /etc/atticd.env && export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64 && atticd -f ${serverConfigFile}"; 10 10 }; 11 11 12 12 makeTestDerivation = pkgs.writeShellScript "make-drv" '' ··· 147 147 148 148 # For testing only - Don't actually do this 149 149 environment.etc."atticd.env".text = '' 150 - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ=" 150 + ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64='LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBekhqUzFGKzlRaFFUdlJZYjZ0UGhxS09FME5VYkIraTJMOTByWVBNQVVoYVBUMmlKCmVUNk9vWFlmZWszZlZ1dXIrYks1VWFVRjhUbEx2Y1FHa1Arckd0WDRiQUpGTWJBcTF3Y25FQ3R6ZGVERHJnSlIKMGUvNWJhdXQwSS9YS0ticG9oYjNvWVhtUmR5eG9WVGE3akY1bk11ajBsd25kUTcwYTF1ZGkzMGNpYkdTWHZMagpVeGltL3ByYjUrV3ZPdjN4UnhlbDZHYmptUW1RMVBHeHVLcmx3b1ZKRnlWTjl3QmExajBDelJDcURnTFRwQWw0CjhLVWlDY2V1VUZQcmdZaW9vSVhyVExlWmxVbFVVV3FHSDBJbGFKeVUyQ05iNWJtZWM1TnZ4RDlaakFoYytucmgKRS80VzkxajdQMFVyQnp4am9NUTRlKzBPZDhmQnBvSDAwbm4xUXdJREFRQUJBb0lCQUE2RmxEK21Ed3gyM1pJRAoxSGJBbHBuQ0IwaEhvbFJVK0Q5OC96d3k5ZlplaU00VWVCTUcyTjFweE1HTWIweStqeWU4UkVJaXJNSGRsbDRECllvNEF3bmUwODZCRUp3TG81cG4vOVl2RjhqelFla1ZNLzkrZm9nRGlmUVUvZWdIMm5NZzR4bHlQNUhOWXdicmEKQ25SNVNoQlRQQzdRQWJOa0hRTFU3bUwrUHowZUlXaG9KWVRoUUpkU0g3RDB0K1QwZzVVNDdPam5qbXJaTWwxaApHOE1IUHhKMk5WU1l2N0dobnpjblZvcVVxYzlxeldXRDZXZERtV1BPNGJ1K2p0b2E2U2o4cjJtb0RRZ1A5YXNhCm93RUFJbHBmbVkxYUx2dENwWG4rejRTTWJKcHRXMlVvaktGa2dkYm9jZmtXYWdtSGZRa2xmS0dBQ0hibU9ZV24KeDRCbTU3a0NnWUVBN1dXaXJDZnBRR01hR3A2WWxMQlVUc1VJSXJOclF4UmtuRlc3dFVYd0NqWFZ5SDlTR3FqNgphTkNhYzZpaks3QVNBYXlxY1JQRjFPY2gyNmxpVmRKUHNuRGxwUjhEVXB2TzRVOVRzSTJyZ1lZYzNrSWkzVGFKClgzV0Vic1Z6Nk45WXFPSXlnVnZiTEhLS0F4Uyt4b1Z2SjkzQmdWRHN5SkxRdmhrM3VubXk3M2tDZ1lFQTNINnYKeUhOKzllOVAyOS9zMVY1eWZxSjdvdVdKV0lBTHFDYm9zOTRRSVdPSG5HRUtSSGkydWIzR0d6U2tRSzN1eTUrdQo4M0txaFJOejRVMkdOK1pLaFE0NHhNVmV4TUVvZzJVU3lTaVZ0cFdqWXBwT2Q1NnVaMzRWaFU2TWRNZS9zT0JnCnNoei84MUxUSis2cHdFZE9wV2tPVlRaMXJISlZXQmdtVk5qWjc1c0NnWUVBNVd5YjBaU2dyMEVYTVRLa2NzNFcKTENudXV0cDZodEZtaWsrd29IZCtpOStMUThFSU1BdXVOUzJrbHJJYlAxVmhrWXkxQzZMNFJkRTV2M2ZyT05XUApmL3ZyYzdDTkhZREdacWlyVUswWldvdXB5b0pQLzBsOWFXdkJHT3hxSUZ2NDZ2M3ZvV1NNWkdBdFVOenpvaGZDClhOeks3WmF2dndka0JOT0tNQVQ5RU1FQ2dZRUF3NEhaWDRWNUo1d2dWVGVDQ2RjSzhsb2tBbFpBcUNZeEw5SUEKTjZ4STVUSVpSb0dNMXhXcC81dlRrci9rZkMwOU5YUExiclZYbVZPY1JrTzFKTStmZDhjYWN1OEdqck11dHdMaAoyMWVQR0N3cWlQMkZZZTlqZVFTRkZJU0hhZXpMZll3V2NSZmhvdURudGRxYXpaRHNuU0kvd1RMZXVCOVFxU0lRCnF0NzByczBDZ1lCQ2lzV0VKdXpQUUlJNzVTVkU4UnJFZGtUeUdhOEVBOHltcStMdDVLRDhPYk80Q2JHYVFlWXkKWFpjSHVyOFg2cW1lWHZVU3MwMHBMMUdnTlJ3WCtSUjNMVDhXTm9vc0NqVDlEUW9GOFZveEtseDROVTRoUGlrTQpBc0w1RS9wYnVLeXkvSU5LTnQyT3ZPZmJYVitlTXZQdGs5c1dORjNyRTBYcU15TW9maG9NaVE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=' 151 151 ''; 152 152 153 153 services.atticd = { ··· 156 156 settings = { 157 157 listen = "[::]:8080"; 158 158 159 + jwt = { }; 160 + 159 161 chunking = { 160 162 nar-size-threshold = 1; 161 163 min-size = 64 * 1024; ··· 165 167 }; 166 168 }; 167 169 168 - environment.systemPackages = [ pkgs.attic-server ]; 170 + environment.systemPackages = [ pkgs.openssl pkgs.attic-server ]; 169 171 170 172 networking.firewall.allowedTCPPorts = [ 8080 ]; 171 173 };
+5 -5
nixos/atticd.nix
··· 16 16 } '' 17 17 cat $configFile 18 18 19 - export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ=" 19 + export ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64="$(${pkgs.openssl}/bin/openssl genrsa -traditional 4096 | ${pkgs.coreutils}/bin/base64 -w0)" 20 20 export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:" 21 21 ${cfg.package}/bin/atticd --mode check-config -f $configFile 22 22 cat <$configFile >$out ··· 79 79 Path to an EnvironmentFile containing required environment 80 80 variables: 81 81 82 - - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64: The Base64-encoded version of the 83 - HS256 JWT secret. Generate it with `openssl rand 64 | base64 -w0`. 82 + - ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64: The base64-encoded RSA PEM PKCS1 of the 83 + RS256 JWT secret. Generate it with `openssl genrsa -traditional 4096 | base64 -w0`. 84 84 ''; 85 85 type = types.nullOr types.path; 86 86 default = null; ··· 154 154 message = '' 155 155 <option>services.atticd.credentialsFile</option> is not set. 156 156 157 - Run `openssl rand 64 | base64 -w0` and create a file with the following contents: 157 + Run `openssl genrsa -traditional -out private_key.pem 4096 | base64 -w0` and create a file with the following contents: 158 158 159 - ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="output from command" 159 + ATTIC_SERVER_TOKEN_RS256_SECRET="output from command" 160 160 161 161 Then, set `services.atticd.credentialsFile` to the quoted absolute path of the file. 162 162 '';
+1
server/Cargo.toml
··· 60 60 uuid = { version = "1.3.3", features = ["v4"] } 61 61 console-subscriber = "0.2.0" 62 62 xdg = "2.5.0" 63 + rsa = "0.9.3" 63 64 64 65 [dependencies.async-compression] 65 66 version = "0.4.0"
+12 -3
server/src/access/http.rs
··· 1 1 //! HTTP middlewares for access control. 2 2 3 + use attic::cache::CacheName; 4 + use attic_token::util::parse_authorization_header; 3 5 use axum::{extract::Request, middleware::Next, response::Response}; 4 6 use sea_orm::DatabaseConnection; 5 7 use tokio::sync::OnceCell; ··· 8 10 use crate::database::{entity::cache::CacheModel, AtticDatabase}; 9 11 use crate::error::ServerResult; 10 12 use crate::{RequestState, State}; 11 - use attic::cache::CacheName; 12 - use attic_token::util::parse_authorization_header; 13 13 14 14 /// Auth state. 15 15 #[derive(Debug)] ··· 101 101 .and_then(parse_authorization_header) 102 102 .and_then(|jwt| { 103 103 let state = req.extensions().get::<State>().unwrap(); 104 - let res_token = Token::from_jwt(&jwt, &state.config.token_hs256_secret); 104 + let signature_type = state.config.jwt.signing_config.clone().into(); 105 + 106 + let res_token = Token::from_jwt( 107 + &jwt, 108 + &signature_type, 109 + &state.config.jwt.token_bound_issuer, 110 + &state.config.jwt.token_bound_audiences, 111 + ); 112 + 105 113 if let Err(e) = &res_token { 106 114 tracing::debug!("Ignoring bad JWT token: {}", e); 107 115 } 116 + 108 117 res_token.ok() 109 118 }); 110 119
+7 -1
server/src/adm/command/make_token.rs
··· 115 115 if sub.dump_claims { 116 116 println!("{}", serde_json::to_string(token.opaque_claims())?); 117 117 } else { 118 - let encoded_token = token.encode(&config.token_hs256_secret)?; 118 + let signature_type = config.jwt.signing_config.into(); 119 + 120 + let encoded_token = token.encode( 121 + &signature_type, 122 + &config.jwt.token_bound_issuer, 123 + &config.jwt.token_bound_audiences, 124 + )?; 119 125 println!("{}", encoded_token); 120 126 } 121 127
+35 -8
server/src/config-template.toml
··· 34 34 # cache. 35 35 #require-proof-of-possession = true 36 36 37 - # JWT signing token 38 - # 39 - # Set this to the Base64 encoding of some random data. 40 - # You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64` environment 41 - # variable. 42 - token-hs256-secret-base64 = "%token_hs256_secret_base64%" 43 - 44 37 # Database connection 45 38 [database] 46 39 # Connection URL ··· 85 78 #[storage.credentials] 86 79 # access_key_id = "" 87 80 # secret_access_key = "" 88 - 81 + 89 82 # Data chunking 90 83 # 91 84 # Warning: If you change any of the values here, it will be ··· 134 127 # Zero (default) means time-based garbage-collection is 135 128 # disabled by default. You can enable it on a per-cache basis. 136 129 #default-retention-period = "6 months" 130 + 131 + [jwt] 132 + # WARNING: Changing _anything_ in this section will break any existing 133 + # tokens. If you need to regenerate them, ensure that you use the the 134 + # correct secret and include the `iss` and `aud` claims. 135 + 136 + # JWT `iss` claim 137 + # 138 + # Set this to the JWT issuer that you want to validate. 139 + # If this is set, all received JWTs will validate that the `iss` claim 140 + # matches this value. 141 + #token-bound-issuer = "some-issuer" 142 + 143 + # JWT `aud` claim 144 + # 145 + # Set this to the JWT audience(s) that you want to validate. 146 + # If this is set, all received JWTs will validate that the `aud` claim 147 + # contains at least one of these values. 148 + #token-bound-audiences = ["some-audience1", "some-audience2"] 149 + 150 + [jwt.signing] 151 + # JWT RS256 secret key 152 + # 153 + # Set this to the base64-encoded private half of an RSA PEM PKCS1 key. 154 + # You can also set it via the `ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64` 155 + # environment variable. 156 + token-rs256-secret-base64 = "%token_rs256_secret_base64%" 157 + 158 + # JWT HS256 secret key 159 + # 160 + # Set this to the base64-encoded HMAC secret key. 161 + # You can also set it via the `ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64` 162 + # environment variable. 163 + #token-hs256-secret-base64 = ""
+209 -12
server/src/config.rs
··· 1 1 //! Server configuration. 2 2 3 + use std::collections::HashSet; 3 4 use std::env; 4 5 use std::net::SocketAddr; 5 6 use std::path::{Path, PathBuf}; ··· 7 8 8 9 use anyhow::Result; 9 10 use async_compression::Level as CompressionLevel; 11 + use attic_token::SignatureType; 10 12 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; 11 13 use derivative::Derivative; 12 14 use serde::{de, Deserialize}; 13 15 use xdg::BaseDirectories; 14 16 15 - use crate::access::{decode_token_hs256_secret_base64, HS256Key}; 17 + use crate::access::{ 18 + decode_token_hs256_secret_base64, decode_token_rs256_pubkey_base64, 19 + decode_token_rs256_secret_base64, HS256Key, RS256KeyPair, RS256PublicKey, 20 + }; 16 21 use crate::narinfo::Compression as NixCompression; 17 22 use crate::storage::{LocalStorageConfig, S3StorageConfig}; 18 23 ··· 26 31 /// This is useful for deploying to certain application platforms like Fly.io 27 32 const ENV_CONFIG_BASE64: &str = "ATTIC_SERVER_CONFIG_BASE64"; 28 33 29 - /// Environment variable storing the Base64-encoded HS256 JWT secret. 34 + /// Environment variable storing the base64-encoded HMAC secret (used for signing and verifying 35 + /// received JWTs). 30 36 const ENV_TOKEN_HS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64"; 37 + 38 + /// Environment variable storing the base64-encoded RSA PEM PKCS1 private key (used for signing and 39 + /// verifying received JWTs). 40 + const ENV_TOKEN_RS256_SECRET_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64"; 41 + 42 + /// Environment variable storing the base64-encoded RSA PEM PKCS1 public key (used for verifying 43 + /// received JWTs only). 44 + const ENV_TOKEN_RS256_PUBKEY_BASE64: &str = "ATTIC_SERVER_TOKEN_RS256_PUBKEY_BASE64"; 31 45 32 46 /// Environment variable storing the database connection string. 33 47 const ENV_DATABASE_URL: &str = "ATTIC_SERVER_DATABASE_URL"; ··· 108 122 #[serde(default = "Default::default")] 109 123 pub garbage_collection: GarbageCollectionConfig, 110 124 125 + /// JSON Web Token. 126 + pub jwt: JWTConfig, 127 + 128 + /// (Deprecated Stub) 129 + /// 130 + /// This simply results in an error telling the user to update 131 + /// their configuration. 132 + #[serde(rename = "token-hs256-secret-base64")] 133 + #[serde(default = "Default::default")] 134 + #[serde(deserialize_with = "deserialize_deprecated_token_hs256_secret")] 135 + #[derivative(Debug = "ignore")] 136 + pub _depreated_token_hs256_secret: Option<String>, 137 + } 138 + 139 + /// JSON Web Token configuration. 140 + #[derive(Clone, Derivative, Deserialize)] 141 + #[derivative(Debug)] 142 + pub struct JWTConfig { 143 + /// The `iss` claim of the JWT. 144 + /// 145 + /// If specified, received JWTs must have this claim, and its value must match this 146 + /// configuration. 147 + #[serde(rename = "token-bound-issuer")] 148 + #[serde(default = "Default::default")] 149 + pub token_bound_issuer: Option<String>, 150 + 151 + /// The `aud` claim of the JWT. 152 + /// 153 + /// If specified, received JWTs must have this claim, and must contain one of the configured 154 + /// values. 155 + #[serde(rename = "token-bound-audiences")] 156 + #[serde(default = "Default::default")] 157 + pub token_bound_audiences: Option<HashSet<String>>, 158 + 159 + /// JSON Web Token signing. 160 + #[serde(rename = "signing")] 161 + #[serde(default = "load_jwt_signing_config_from_env")] 162 + #[derivative(Debug = "ignore")] 163 + pub signing_config: JWTSigningConfig, 164 + } 165 + 166 + /// JSON Web Token signing configuration. 167 + #[derive(Clone, Deserialize)] 168 + pub enum JWTSigningConfig { 169 + /// JSON Web Token RSA pubkey. 170 + /// 171 + /// Set this to the base64-encoded RSA PEM PKCS1 public key to use for verifying JWTs only. 172 + #[serde(rename = "token-rs256-pubkey-base64")] 173 + #[serde(deserialize_with = "deserialize_token_rs256_pubkey_base64")] 174 + RS256VerifyOnly(RS256PublicKey), 175 + 176 + /// JSON Web Token RSA secret. 177 + /// 178 + /// Set this to the base64-encoded RSA PEM PKCS1 private key to use for signing and verifying 179 + /// JWTs. 180 + #[serde(rename = "token-rs256-secret-base64")] 181 + #[serde(deserialize_with = "deserialize_token_rs256_secret_base64")] 182 + RS256SignAndVerify(RS256KeyPair), 183 + 111 184 /// JSON Web Token HMAC secret. 112 185 /// 113 - /// Set this to the base64 encoding of a randomly generated secret. 186 + /// Set this to the base64-encoded HMAC secret to use for signing and verifying JWTs. 114 187 #[serde(rename = "token-hs256-secret-base64")] 115 188 #[serde(deserialize_with = "deserialize_token_hs256_secret_base64")] 116 - #[serde(default = "load_token_hs256_secret_from_env")] 117 - #[derivative(Debug = "ignore")] 118 - pub token_hs256_secret: HS256Key, 189 + HS256SignAndVerify(HS256Key), 190 + } 191 + 192 + impl From<JWTSigningConfig> for SignatureType { 193 + fn from(value: JWTSigningConfig) -> Self { 194 + match value { 195 + JWTSigningConfig::RS256VerifyOnly(key) => Self::RS256PubkeyOnly(key), 196 + JWTSigningConfig::RS256SignAndVerify(key) => Self::RS256(key), 197 + JWTSigningConfig::HS256SignAndVerify(key) => Self::HS256(key), 198 + } 199 + } 119 200 } 120 201 121 202 /// Database connection configuration. ··· 240 321 pub default_retention_period: Duration, 241 322 } 242 323 243 - fn load_token_hs256_secret_from_env() -> HS256Key { 244 - let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64) 245 - .expect("The HS256 secret must be specified in either token_hs256_secret or the ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64 environment."); 324 + fn load_jwt_signing_config_from_env() -> JWTSigningConfig { 325 + let config = if let Some(config) = load_token_rs256_pubkey_from_env() { 326 + config 327 + } else if let Some(config) = load_token_rs256_secret_from_env() { 328 + config 329 + } else if let Some(config) = load_token_hs256_secret_from_env() { 330 + config 331 + } else { 332 + panic!( 333 + "\n\ 334 + You must configure JWT signing and verification inside your TOML \ 335 + configuration by setting one of the following options in the \ 336 + [jwt.signing] block:\n\ 337 + \n\ 338 + * token-rs256-pubkey-base64\n\ 339 + * token-rs256-secret-base64\n\ 340 + * token-hs256-secret-base64\n\ 341 + \n\ 342 + or by setting one of the following environment variables:\n\ 343 + \n\ 344 + * {ENV_TOKEN_RS256_PUBKEY_BASE64}\n\ 345 + * {ENV_TOKEN_RS256_SECRET_BASE64}\n\ 346 + * {ENV_TOKEN_HS256_SECRET_BASE64}\n\ 347 + \n\ 348 + Options will be tried in that same order (configuration options \ 349 + first, then environment options if none of the configuration options \ 350 + were set, starting with the respective RSA pubkey option, the RSA \ 351 + secret option, and finally the HMAC secret option). \ 352 + The first option that is found will be used.\n\ 353 + \n\ 354 + If an RS256 pubkey (asymmetric RSA PEM PKCS1 public key) is \ 355 + provided, it will only be possible to verify received JWTs, and not \ 356 + sign new JWTs.\n\ 357 + \n\ 358 + If an RS256 secret (asymmetric RSA PEM PKCS1 private key) is \ 359 + provided, it will be used for both signing new JWTs and verifying \ 360 + received JWTs.\n\ 361 + \n\ 362 + If an HS256 secret (symmetric HMAC secret) is provided, it will be \ 363 + used for both signing new JWTs and verifying received JWTs.\n\ 364 + " 365 + ) 366 + }; 367 + 368 + config 369 + } 370 + 371 + fn load_token_hs256_secret_from_env() -> Option<JWTSigningConfig> { 372 + let s = env::var(ENV_TOKEN_HS256_SECRET_BASE64).ok()?; 373 + 374 + decode_token_hs256_secret_base64(&s) 375 + .ok() 376 + .map(JWTSigningConfig::HS256SignAndVerify) 377 + } 378 + 379 + fn load_token_rs256_secret_from_env() -> Option<JWTSigningConfig> { 380 + let s = env::var(ENV_TOKEN_RS256_SECRET_BASE64).ok()?; 381 + 382 + decode_token_rs256_secret_base64(&s) 383 + .ok() 384 + .map(JWTSigningConfig::RS256SignAndVerify) 385 + } 386 + 387 + fn load_token_rs256_pubkey_from_env() -> Option<JWTSigningConfig> { 388 + let s = env::var(ENV_TOKEN_RS256_PUBKEY_BASE64).ok()?; 246 389 247 - decode_token_hs256_secret_base64(&s).expect("Failed to load as decoding key") 390 + decode_token_rs256_pubkey_base64(&s) 391 + .ok() 392 + .map(JWTSigningConfig::RS256VerifyOnly) 248 393 } 249 394 250 395 fn load_database_url_from_env() -> String { 251 - env::var(ENV_DATABASE_URL) 252 - .expect("Database URL must be specified in either database.url or the ATTIC_SERVER_DATABASE_URL environment.") 396 + env::var(ENV_DATABASE_URL).expect(&format!( 397 + "Database URL must be specified in either database.url \ 398 + or the {ENV_DATABASE_URL} environment." 399 + )) 253 400 } 254 401 255 402 impl CompressionConfig { ··· 296 443 } 297 444 } 298 445 446 + fn deserialize_deprecated_token_hs256_secret<'de, D>( 447 + _deserializer: D, 448 + ) -> Result<Option<String>, D::Error> 449 + where 450 + D: de::Deserializer<'de>, 451 + { 452 + use de::Error; 453 + 454 + Err(Error::custom( 455 + "\n\ 456 + The token-hs256-secret-base64 field has been moved to [jwt.signing].\n\ 457 + \n\ 458 + To continue using HS256 signing, move your current config:\n\ 459 + \n\ 460 + token-hs256-secret-base64 = \"your token\"\n\ 461 + \n\ 462 + To the bottom of the file like so:\n\ 463 + \n\ 464 + [jwt.signing]\n\ 465 + token-hs256-secret-base64 = \"your token\"\n\ 466 + ", 467 + )) 468 + } 469 + 299 470 fn deserialize_token_hs256_secret_base64<'de, D>(deserializer: D) -> Result<HS256Key, D::Error> 300 471 where 301 472 D: de::Deserializer<'de>, ··· 304 475 305 476 let s = String::deserialize(deserializer)?; 306 477 let key = decode_token_hs256_secret_base64(&s).map_err(Error::custom)?; 478 + 479 + Ok(key) 480 + } 481 + 482 + fn deserialize_token_rs256_secret_base64<'de, D>(deserializer: D) -> Result<RS256KeyPair, D::Error> 483 + where 484 + D: de::Deserializer<'de>, 485 + { 486 + use de::Error; 487 + 488 + let s = String::deserialize(deserializer)?; 489 + let key = decode_token_rs256_secret_base64(&s).map_err(Error::custom)?; 490 + 491 + Ok(key) 492 + } 493 + 494 + fn deserialize_token_rs256_pubkey_base64<'de, D>( 495 + deserializer: D, 496 + ) -> Result<RS256PublicKey, D::Error> 497 + where 498 + D: de::Deserializer<'de>, 499 + { 500 + use de::Error; 501 + 502 + let s = String::deserialize(deserializer)?; 503 + let key = decode_token_rs256_pubkey_base64(&s).map_err(Error::custom)?; 307 504 308 505 Ok(key) 309 506 }
+10 -13
server/src/oobe.rs
··· 14 14 use anyhow::Result; 15 15 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; 16 16 use chrono::{Months, Utc}; 17 - use rand::distributions::Alphanumeric; 18 - use rand::Rng; 17 + use rsa::pkcs1::EncodeRsaPrivateKey; 19 18 use tokio::fs::{self, OpenOptions}; 20 19 21 - use crate::access::{decode_token_hs256_secret_base64, Token}; 20 + use crate::access::{decode_token_rs256_secret_base64, SignatureType, Token}; 22 21 use crate::config; 23 22 use attic::cache::CacheNamePattern; 24 23 ··· 45 44 let storage_path = data_path.join("storage"); 46 45 fs::create_dir_all(&storage_path).await?; 47 46 48 - let hs256_secret_base64 = { 49 - let random: String = rand::thread_rng() 50 - .sample_iter(&Alphanumeric) 51 - .take(128) 52 - .map(char::from) 53 - .collect(); 47 + let rs256_secret_base64 = { 48 + let mut rng = rand::thread_rng(); 49 + let private_key = rsa::RsaPrivateKey::new(&mut rng, 4096)?; 50 + let pkcs1_pem = private_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)?; 54 51 55 - BASE64_STANDARD.encode(random) 52 + BASE64_STANDARD.encode(pkcs1_pem.as_bytes()) 56 53 }; 57 54 58 55 let config_content = CONFIG_TEMPLATE 59 56 .replace("%database_url%", &database_url) 60 57 .replace("%storage_path%", storage_path.to_str().unwrap()) 61 - .replace("%token_hs256_secret_base64%", &hs256_secret_base64); 58 + .replace("%token_rs256_secret_base64%", &rs256_secret_base64); 62 59 63 60 fs::write(&config_path, config_content.as_bytes()).await?; 64 61 ··· 76 73 perm.configure_cache_retention = true; 77 74 perm.destroy_cache = true; 78 75 79 - let key = decode_token_hs256_secret_base64(&hs256_secret_base64).unwrap(); 80 - token.encode(&key)? 76 + let key = decode_token_rs256_secret_base64(&rs256_secret_base64).unwrap(); 77 + token.encode(&SignatureType::RS256(key), &None, &None)? 81 78 }; 82 79 83 80 eprintln!();
+2 -1
token/Cargo.toml
··· 9 9 attic = { path = "../attic", default-features = false } 10 10 11 11 base64 = "0.22.1" 12 - chrono = "0.4.24" 12 + chrono = "0.4.31" 13 13 displaydoc = "0.2.4" 14 14 indexmap = { version = "2.2.6", features = ["serde"] } 15 15 jwt-simple = "0.11.5" ··· 18 18 serde = "1.0.163" 19 19 serde_with = "3.0.0" 20 20 tracing = "0.1.37" 21 + rsa = "0.9.3"
+114 -21
token/src/lib.rs
··· 1 1 //! Access control. 2 2 //! 3 3 //! Access control in Attic is simple and stateless [0] - The server validates 4 - //! the JWT against a HS256 key and allows access based on the `https://jwt.attic.rs/v1` 4 + //! the JWT against the configured key and allows access based on the `https://jwt.attic.rs/v1` 5 5 //! claim. 6 6 //! 7 7 //! One primary goal of the Attic Server is easy scalability. It's designed ··· 83 83 #[cfg(test)] 84 84 mod tests; 85 85 86 + use std::collections::HashSet; 86 87 use std::error::Error as StdError; 87 88 88 89 use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine}; 89 90 use chrono::{DateTime, Utc}; 90 91 use displaydoc::Display; 91 92 use indexmap::IndexMap; 93 + use jwt_simple::prelude::{Duration, RSAKeyPairLike, RSAPublicKeyLike, VerificationOptions}; 92 94 pub use jwt_simple::{ 93 - algorithms::{HS256Key, MACLike}, 95 + algorithms::{HS256Key, MACLike, RS256KeyPair, RS256PublicKey}, 94 96 claims::{Claims, JWTClaims}, 95 97 prelude::UnixTimeStamp, 96 98 }; ··· 155 157 pub struct CachePermission { 156 158 /// Can pull objects from the cache. 157 159 #[serde(default = "CachePermission::permission_default")] 158 - #[serde(skip_serializing_if = "is_false")] 160 + #[serde(skip_serializing_if = "std::ops::Not::not")] 159 161 #[serde(rename = "r")] 160 162 #[serde_as(as = "BoolFromInt")] 161 163 pub pull: bool, 162 164 163 165 /// Can push objects to the cache. 164 166 #[serde(default = "CachePermission::permission_default")] 165 - #[serde(skip_serializing_if = "is_false")] 167 + #[serde(skip_serializing_if = "std::ops::Not::not")] 166 168 #[serde(rename = "w")] 167 169 #[serde_as(as = "BoolFromInt")] 168 170 pub push: bool, 169 171 170 172 /// Can delete objects from the cache. 171 173 #[serde(default = "CachePermission::permission_default")] 172 - #[serde(skip_serializing_if = "is_false")] 174 + #[serde(skip_serializing_if = "std::ops::Not::not")] 173 175 #[serde(rename = "d")] 174 176 #[serde_as(as = "BoolFromInt")] 175 177 pub delete: bool, 176 178 177 179 /// Can create the cache itself. 178 180 #[serde(default = "CachePermission::permission_default")] 179 - #[serde(skip_serializing_if = "is_false")] 181 + #[serde(skip_serializing_if = "std::ops::Not::not")] 180 182 #[serde(rename = "cc")] 181 183 #[serde_as(as = "BoolFromInt")] 182 184 pub create_cache: bool, 183 185 184 186 /// Can reconfigure the cache. 185 187 #[serde(default = "CachePermission::permission_default")] 186 - #[serde(skip_serializing_if = "is_false")] 188 + #[serde(skip_serializing_if = "std::ops::Not::not")] 187 189 #[serde(rename = "cr")] 188 190 #[serde_as(as = "BoolFromInt")] 189 191 pub configure_cache: bool, 190 192 191 193 /// Can configure retention/quota settings. 192 194 #[serde(default = "CachePermission::permission_default")] 193 - #[serde(skip_serializing_if = "is_false")] 195 + #[serde(skip_serializing_if = "std::ops::Not::not")] 194 196 #[serde(rename = "cq")] 195 197 #[serde_as(as = "BoolFromInt")] 196 198 pub configure_cache_retention: bool, 197 199 198 200 /// Can destroy the cache itself. 199 201 #[serde(default = "CachePermission::permission_default")] 200 - #[serde(skip_serializing_if = "is_false")] 202 + #[serde(skip_serializing_if = "std::ops::Not::not")] 201 203 #[serde(rename = "cd")] 202 204 #[serde_as(as = "BoolFromInt")] 203 205 pub destroy_cache: bool, ··· 223 225 224 226 /// Base64 decode error: {0} 225 227 Base64Error(base64::DecodeError), 228 + 229 + /// RSA Key error: {0} 230 + RsaKeyError(rsa::pkcs1::Error), 231 + 232 + /// Failure decoding the base64 layer of the base64 encoded PEM 233 + Utf8Error(std::str::Utf8Error), 234 + 235 + /// Pubkey-only JWT authentication cannot create signed JWTs 236 + PubkeyOnlyCannotCreateToken, 237 + } 238 + 239 + /// The supported JWT signature types. 240 + pub enum SignatureType { 241 + HS256(HS256Key), 242 + RS256(RS256KeyPair), 243 + RS256PubkeyOnly(RS256PublicKey), 226 244 } 227 245 228 246 impl Token { 229 247 /// Verifies and decodes a token. 230 - pub fn from_jwt(token: &str, key: &HS256Key) -> Result<Self> { 231 - key.verify_token(token, None) 232 - .map_err(Error::TokenError) 233 - .map(Token) 248 + pub fn from_jwt( 249 + token: &str, 250 + signature_type: &SignatureType, 251 + maybe_bound_issuer: &Option<String>, 252 + maybe_bound_audiences: &Option<HashSet<String>>, 253 + ) -> Result<Self> { 254 + let opts = VerificationOptions { 255 + reject_before: None, 256 + accept_future: false, 257 + required_subject: None, 258 + required_key_id: None, 259 + required_public_key: None, 260 + required_nonce: None, 261 + allowed_issuers: maybe_bound_issuer 262 + .as_ref() 263 + .map(|s| [s.to_owned()].into()) 264 + .to_owned(), 265 + allowed_audiences: maybe_bound_audiences.to_owned(), 266 + time_tolerance: None, 267 + max_validity: None, 268 + max_token_length: None, 269 + max_header_length: None, 270 + artificial_time: None, 271 + }; 272 + 273 + match signature_type { 274 + SignatureType::HS256(key) => key 275 + .verify_token(token, Some(opts)) 276 + .map_err(Error::TokenError) 277 + .map(Token), 278 + SignatureType::RS256(key) => { 279 + let public_key = key.public_key(); 280 + public_key 281 + .verify_token(token, Some(opts)) 282 + .map_err(Error::TokenError) 283 + .map(Token) 284 + } 285 + SignatureType::RS256PubkeyOnly(key) => key 286 + .verify_token(token, Some(opts)) 287 + .map_err(Error::TokenError) 288 + .map(Token), 289 + } 234 290 } 235 291 236 292 /// Creates a new token with an expiration timestamp. ··· 239 295 attic_ns: Default::default(), 240 296 }; 241 297 298 + let now_epoch = Utc::now().signed_duration_since(DateTime::UNIX_EPOCH); 299 + 242 300 Self(JWTClaims { 243 301 issued_at: None, 244 302 expires_at: Some(UnixTimeStamp::from_secs( 245 303 exp.timestamp().try_into().unwrap(), 246 304 )), 247 - invalid_before: None, 305 + invalid_before: Some(Duration::new( 306 + now_epoch.num_seconds().try_into().unwrap(), 307 + 0, 308 + )), 248 309 issuer: None, 249 310 subject: Some(sub), 250 311 audiences: None, ··· 255 316 } 256 317 257 318 /// Encodes the token. 258 - pub fn encode(&self, key: &HS256Key) -> Result<String> { 259 - key.authenticate(self.0.clone()).map_err(Error::TokenError) 319 + pub fn encode( 320 + &self, 321 + signature_type: &SignatureType, 322 + maybe_bound_issuer: &Option<String>, 323 + maybe_bound_audiences: &Option<HashSet<String>>, 324 + ) -> Result<String> { 325 + let mut token = self.0.clone(); 326 + 327 + if let Some(issuer) = maybe_bound_issuer { 328 + token = token.with_issuer(issuer); 329 + } 330 + if let Some(audiences) = maybe_bound_audiences { 331 + token = token.with_audiences(audiences.to_owned()); 332 + } 333 + 334 + match signature_type { 335 + SignatureType::HS256(key) => key.authenticate(token).map_err(Error::TokenError), 336 + SignatureType::RS256(key) => key.sign(token).map_err(Error::TokenError), 337 + SignatureType::RS256PubkeyOnly(_) => { 338 + return Err(Error::PubkeyOnlyCannotCreateToken); 339 + } 340 + } 260 341 } 261 342 262 343 /// Returns the subject of the token. ··· 362 443 impl StdError for Error {} 363 444 364 445 pub fn decode_token_hs256_secret_base64(s: &str) -> Result<HS256Key> { 365 - let secret = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?; 366 - Ok(HS256Key::from_bytes(&secret)) 446 + let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?; 447 + let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?; 448 + Ok(HS256Key::from_bytes(&secret.as_bytes())) 367 449 } 368 450 369 - // bruh 370 - fn is_false(b: &bool) -> bool { 371 - !b 451 + pub fn decode_token_rs256_secret_base64(s: &str) -> Result<RS256KeyPair> { 452 + let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?; 453 + let secret = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?; 454 + let keypair = RS256KeyPair::from_pem(secret).map_err(Error::TokenError)?; 455 + 456 + Ok(keypair) 457 + } 458 + 459 + pub fn decode_token_rs256_pubkey_base64(s: &str) -> Result<RS256PublicKey> { 460 + let decoded = BASE64_STANDARD.decode(s).map_err(Error::Base64Error)?; 461 + let pubkey = std::str::from_utf8(&decoded).map_err(Error::Utf8Error)?; 462 + let pubkey = RS256PublicKey::from_pem(pubkey).map_err(Error::TokenError)?; 463 + 464 + Ok(pubkey) 372 465 }
+86 -56
token/src/tests.rs
··· 10 10 11 11 #[test] 12 12 fn test_basic() { 13 - // "very secure secret" 14 - let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0"; 15 - 16 - let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap(); 17 - 18 13 /* 14 + $ cat json 19 15 { 20 16 "sub": "meow", 21 17 "exp": 4102324986, 18 + "nbf": 0, 22 19 "https://jwt.attic.rs/v1": { 23 20 "caches": { 24 21 "all-*": {"r":1}, ··· 31 28 } 32 29 */ 33 30 34 - let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw"; 31 + let tokens: &[(&str, Box<dyn Fn() -> Token>)] = &[ 32 + ( 33 + "hs256", 34 + Box::new(|| { 35 + // "very secure secret" 36 + let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0"; 37 + let dec_key = decode_token_hs256_secret_base64(base64_secret).unwrap(); 38 + 39 + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MTY2NjA1ODksInN1YiI6Im1lb3cifQ.8vtxp_1OEYdcnkGPM4c9ORXooJZV7DOTS4NRkMKN8mw"; 40 + 41 + Token::from_jwt(token, &SignatureType::HS256(dec_key), &None, &None).unwrap() 42 + }), 43 + ), 44 + ( 45 + "rs256", 46 + Box::new(|| { 47 + // nix shell nixpkgs#jwt-cli 48 + // openssl genpkey -out rs256 -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform der 49 + // BASE64_SECRET=$(openssl rsa -in rs256 -outform PEM -traditional | base64 -w0) 50 + let base64_secret = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNUZranRMRzV5eS9pMFlnYkQxeUJBK21GckNmLzZiQ2F0TDFFQ3ppNG1tZWhSZTcwCkFEL0dSSHhTVUErc0pZeCtZNjlyL0RqQWs2OFJlQ1c4b2FQWXhtc21RNG5VM2ZwZ2E3WWFqZ3ZoWmVsa3JtaC8KZ1ZURWtFTG1IZlJtQkwvOWlsT20yRHNtYTVhUFo0SFl6ellpdjJvcFF5UGRndXcyWXFtbzE3Nk5MdllCMmpJTwovR3FkdE55K3NPV296NktVSVlJa0hWWU5HMENVcFNzdXBqUTJ6VTVZMFc2UXlNQWFWd1BONElJT3lXWUNwZXRECjFJbWxYekhROXM4NXFSWnlLa21iZFhtTVBVWmUvekRxc2FFd3lscFlpT0RjbDdRYU5QTzEzZnk3UGtQMmVwdUkKTk5tZ1E0WEF0MkF4ZXNKck5ibUs4aG1iM3doRXZkNjRFMGdEV1FJREFRQUJBb0lCQUJEemNRd2IyVi8wK1JCMgoyeE5qMll2eHpPTi93S2FYWHBTbUxDUHRIUDhSVEU2RnM0VkZOckdrelBOMmhsL3ZNdjZ4YWdHNk1NbUZ5SFV6CnovSHIyTTY1NjRnOTloaFlXc29FSmFwL3hVYXNjYlhrdWZwZTBZeW4rcThra21JdDRtTmZYRlpXNWI0ODJmNWsKRERVdG5weTVBOEVoSzNOcGw0dnhia0E5dS90TlVlT1NHTkhPYVZjcHdERVhDNXJ4bmFxTm5wMkMwa1A4ODRINgpSb2lZVkF4bytHaVpNVzhIOFRmSXVsenh3c04yQnVNcUNmOGVhNG1EM0pRVHZ2REhhUHM4eVJTUlB3UmlHYUkzCnVybFRmdjg4U20va09oL0N2SkpoRnhCVkVNVjIydWRNUmU3L3NpTWtlbVlvUnhaTWJjRGVQK2h1RktJWTRSMEoKNnRJUHQ3VUNnWUVBOTlhL2IzeFBsQWh0ck02dUlUUXNQd0FYQUg3Q1NXL1FSdVJUTWVhYXVIMk9sRitjZmpMNApJS1Nsdy9QaUtaUEk1TFRWM2ZVZk5WNTVsOFZHTytsT2ViTFhnaXBYM3BqSDBma3AyY3Q2Smk3aGw0aUlXK0h0ClpJNE9KYkYwTTBETHdySkd3T25QL2trRHNxSW9IbC9MdTBRM2FxSm1RVCsvcG54R083R21kbDhDZ1lFQTY5NFcKZHF2NnF4VjF5V0Z4QWZOOE1hZStpTC9xY1VhTm85ZzMva2YvOXZ3VXdtcERvR0xnaVVLMWZKb3BUYlBjcWgwRwptbUZEQ3V2M1Q0OS9yU2k5dU4zYm82cmlXRUl4VFg1YUtFSjlpSEFMWDJGWDdGSDJRdUZGWEwzQ2c0ckdvL1pDCmdjUkxuS3dma3JUVnRxeEdaNjN4YmsvcFpHWjZtTW01VkNDck1VY0NnWUVBc3JUT1pQMG1CSC92VldQU2UyNjcKV05JZncrT2pCSUR6bGFxZHNxV3Rlc3BPUFA2VVFRdFBqM29wYlJvMlFmU21Md09XRXUzbEN2Nk1mcnRvNFZwaAprNjg1WmtwU0FkZjRmWmRFYmg4aWZOWGhKUHIyR0FyWXVtRVVJbW5LZUFxSTRtTGFVZEJHZ2Z6MEJhS1hldzlvClFDZjRMWlBjVjhBMzJUeFRDRWdZMTlFQ2dZQU04U2F5WkVWZzFkQ2N1Q2dIUDJEMUtJc2YzY2Z6WnplbVlkclEKclFxeWRxcDg4Rys5Z1M5bzJLdzBwaERXSHFSaEFTNjNrZGFuNXNLdkx1U0dqOUc1THhNNks4bzNwWW9uQW1QWQpDYTN4cXBRMUs1WXpkVnZaMTVxQ3VEYlFHUEZGVmVIWVZQa0JJOENud0J4cDVaSUhabGYxQVpXQTJNNnBTNGhMCndXOGpTUUtCZ1FDQmNJbjU4Y0lmZkhmMjM4SUJvZnR1UVVzREZGcnkzaUVpaWpTYmJ1WnB1Vm8zL2pWbUsyaEYKS2xUL2xoRDdWdGJ1V3phMG9WQmZDaWZqMnZ2S2pmZ0l6NnF3Um1UbC9DSjlWdUNHTUI1VG55cGl3OEtodXorSAo0L2twdDdNcW9WQ0dRSjd1WVQyQzY1K0JqNklnUnBQT09za3VKNW1RZ0FlbTQ3eDBrVnRSemc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="; 51 + 52 + let dec_key = decode_token_rs256_secret_base64(base64_secret).unwrap(); 53 + 54 + // TOKEN=$(jq -c < json | jwt encode --alg RS256 --secret @./rs256 -) 55 + let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjQxMDIzMjQ5ODYsImh0dHBzOi8vand0LmF0dGljLnJzL3YxIjp7ImNhY2hlcyI6eyJhbGwtKiI6eyJyIjoxfSwiYWxsLWNpLSoiOnsidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJjYWNoZS1ydyI6eyJyIjoxLCJ3IjoxfSwidGVhbS0qIjp7ImNjIjoxLCJyIjoxLCJ3IjoxfX19LCJpYXQiOjE3MjIwMDUwNzksIm5iZiI6MCwic3ViIjoibWVvdyJ9.Zs24IUbQOpOjhEe0sfsoSSJhDrzf4v-_wX_ceKqHeb2MERY8XSIQ1RPTNVeOW4LfJHumJj_rxh8Wv2BRGZSMldrTt0Ab_N7FnkhA37_jnRvgvEjSG3V4fC8aA4KoOa-43NRpg4HmPxiXte5-6LneBOR94Wss868wC1b_2yX2zCc1wQoZA3LNo-CRLnL4Yp5wY4Bbgyguv_9mfqXVYZykZnxumyGwVFD-Rub3KQ9d53Rf9tKcvRk9qxO2q8F2PKjeaUBG2xZtGwkWTMvSmwR1dKtkPUyPggOzbLoUG-6fxfo7D3NyL5qWCSN_7CkI-xlsRSLY1gTq-FqXvcpHeZbc8w"; 56 + 57 + Token::from_jwt(token, &SignatureType::RS256(dec_key), &None, &None).unwrap() 58 + }), 59 + ), 60 + ]; 61 + 62 + for (name, decode) in tokens { 63 + eprintln!("Testing {name}"); 35 64 36 - // NOTE(cole-h): check that we get a consistent iteration order when getting permissions for 37 - // caches -- this depends on the order of the fields in the token, but should otherwise be 38 - // consistent between iterations 39 - let mut was_ever_wrong = false; 40 - for _ in 0..=1_000 { 41 - // NOTE(cole-h): we construct a new Token every iteration in order to get different "random 42 - // state" 43 - let decoded = Token::from_jwt(token, &dec_key).unwrap(); 44 - let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" }); 65 + // NOTE(cole-h): check that we get a consistent iteration order when getting permissions for 66 + // caches -- this depends on the order of the fields in the token, but should otherwise be 67 + // consistent between iterations 68 + let mut was_ever_wrong = false; 69 + for _ in 0..=1_000 { 70 + // NOTE(cole-h): we construct a new Token every iteration in order to get different "random 71 + // state" 72 + let decoded = decode(); 73 + let perm_all_ci = decoded.get_permission_for_cache(&cache! { "all-ci-abc" }); 45 74 46 - // NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be 47 - // retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though 48 - // the `all-*` pattern (which only allows reading/pulling) is specified first 49 - if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() { 50 - was_ever_wrong = true; 75 + // NOTE(cole-h): if the iteration order of the token is inconsistent, the permissions may be 76 + // retrieved from the `all-ci-*` pattern (which only allows writing/pushing), even though 77 + // the `all-*` pattern (which only allows reading/pulling) is specified first 78 + if perm_all_ci.require_pull().is_err() || perm_all_ci.require_push().is_ok() { 79 + was_ever_wrong = true; 80 + } 51 81 } 52 - } 53 - assert!( 54 - !was_ever_wrong, 55 - "Iteration order should be consistent to prevent random auth failures (and successes)" 56 - ); 82 + assert!( 83 + !was_ever_wrong, 84 + "Iteration order should be consistent to prevent random auth failures (and successes)" 85 + ); 57 86 58 - let decoded = Token::from_jwt(token, &dec_key).unwrap(); 87 + let decoded = decode(); 59 88 60 - let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" }); 89 + let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" }); 61 90 62 - assert!(perm_rw.pull); 63 - assert!(perm_rw.push); 64 - assert!(!perm_rw.delete); 65 - assert!(!perm_rw.create_cache); 91 + assert!(perm_rw.pull); 92 + assert!(perm_rw.push); 93 + assert!(!perm_rw.delete); 94 + assert!(!perm_rw.create_cache); 66 95 67 - assert!(perm_rw.require_pull().is_ok()); 68 - assert!(perm_rw.require_push().is_ok()); 69 - assert!(perm_rw.require_delete().is_err()); 70 - assert!(perm_rw.require_create_cache().is_err()); 96 + assert!(perm_rw.require_pull().is_ok()); 97 + assert!(perm_rw.require_push().is_ok()); 98 + assert!(perm_rw.require_delete().is_err()); 99 + assert!(perm_rw.require_create_cache().is_err()); 71 100 72 - let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" }); 101 + let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" }); 73 102 74 - assert!(perm_ro.pull); 75 - assert!(!perm_ro.push); 76 - assert!(!perm_ro.delete); 77 - assert!(!perm_ro.create_cache); 103 + assert!(perm_ro.pull); 104 + assert!(!perm_ro.push); 105 + assert!(!perm_ro.delete); 106 + assert!(!perm_ro.create_cache); 78 107 79 - assert!(perm_ro.require_pull().is_ok()); 80 - assert!(perm_ro.require_push().is_err()); 81 - assert!(perm_ro.require_delete().is_err()); 82 - assert!(perm_ro.require_create_cache().is_err()); 108 + assert!(perm_ro.require_pull().is_ok()); 109 + assert!(perm_ro.require_push().is_err()); 110 + assert!(perm_ro.require_delete().is_err()); 111 + assert!(perm_ro.require_create_cache().is_err()); 83 112 84 - let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" }); 113 + let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" }); 85 114 86 - assert!(perm_team.pull); 87 - assert!(perm_team.push); 88 - assert!(!perm_team.delete); 89 - assert!(perm_team.create_cache); 115 + assert!(perm_team.pull); 116 + assert!(perm_team.push); 117 + assert!(!perm_team.delete); 118 + assert!(perm_team.create_cache); 90 119 91 - assert!(perm_team.require_pull().is_ok()); 92 - assert!(perm_team.require_push().is_ok()); 93 - assert!(perm_team.require_delete().is_err()); 94 - assert!(perm_team.require_create_cache().is_ok()); 120 + assert!(perm_team.require_pull().is_ok()); 121 + assert!(perm_team.require_push().is_ok()); 122 + assert!(perm_team.require_delete().is_err()); 123 + assert!(perm_team.require_create_cache().is_ok()); 95 124 96 - assert!(!decoded 97 - .get_permission_for_cache(&cache! { "forbidden-cache" }) 98 - .can_discover()); 125 + assert!(!decoded 126 + .get_permission_for_cache(&cache! { "forbidden-cache" }) 127 + .can_discover()); 128 + } 99 129 }