diff --git a/Cargo.lock b/Cargo.lock index 16e9a0371..5ed85fe09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,11 @@ dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bincode" version = "0.8.0" @@ -846,12 +851,12 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "5.0.1" -source = "git+https://github.com/Jake-Shadle/jsonwebtoken.git?rev=2f469a61#2f469a61ee31b02cb6b6c3d55515592e9aaeec3b" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1691,13 +1696,15 @@ dependencies = [ [[package]] name = "ring" -version = "0.13.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cc 1.0.47 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1777,7 +1784,7 @@ dependencies = [ "arraydeque 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "assert_cmd 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", - "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1801,7 +1808,7 @@ dependencies = [ "hyperx 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)", "jobserver 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonwebtoken 5.0.1 (git+https://github.com/Jake-Shadle/jsonwebtoken.git?rev=2f469a61)", + "jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", "libmount 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1820,7 +1827,7 @@ dependencies = [ "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.22 (registry+https://github.com/rust-lang/crates.io-index)", "retry 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", "rouille 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "selenium-rs 0.1.1 (git+https://github.com/saresend/selenium-rs.git?rev=0314a2420da78cce7454a980d862995750771722)", "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1845,6 +1852,7 @@ dependencies = [ "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "tower 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", "version-compare 0.0.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2910,6 +2918,7 @@ dependencies = [ "checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea" "checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" "checksum bincode 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e103c8b299b28a9c6990458b7013dc4a8356a9b854c51b9883241f5866fac36e" "checksum bincode 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8ab639324e3ee8774d296864fbc0dbbb256cf1a41c490b94cba90c082915f92" @@ -2994,7 +3003,7 @@ dependencies = [ "checksum itertools 0.7.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0d47946d458e94a1b7bcabbf6521ea7c037062c81f534615abcad76e84d4970d" "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" "checksum jobserver 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b1d42ef453b30b7387e113da1c83ab1605d90c5b4e0eb8e96d016ed3b8c160" -"checksum jsonwebtoken 5.0.1 (git+https://github.com/Jake-Shadle/jsonwebtoken.git?rev=2f469a61)" = "" +"checksum jsonwebtoken 6.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a81d1812d731546d2614737bee92aa071d37e9afa1409bc374da9e5e70e70b22" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" @@ -3085,7 +3094,7 @@ dependencies = [ "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" "checksum reqwest 0.9.22 (registry+https://github.com/rust-lang/crates.io-index)" = "2c2064233e442ce85c77231ebd67d9eca395207dec2127fe0bbedde4bd29a650" "checksum retry 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "29460f6011a25fc70b22010e796bd98330baccaa0005cba6f90b858a510dec0d" -"checksum ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c4db68a2e35f3497146b7e4563df7d4773a2433230c5e4b448328e31740458a" +"checksum ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)" = "426bc186e3e95cac1e4a4be125a4aca7e84c2d616ffc02244eef36e2a60a093c" "checksum rouille 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0845b9c39ba772da769fe2aaa4d81bfd10695a7ea051d0510702260ff4159841" "checksum rust-argon2 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4ca4eaef519b494d1f2848fc602d18816fed808a981aedf4f1f00ceb7c9d32cf" "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" diff --git a/Cargo.toml b/Cargo.toml index be5728760..76d132a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ required-features = ["dist-server"] [dependencies] ar = { version = "0.6", optional = true } atty = "0.2.6" -base64 = "0.9.0" +base64 = "0.11.0" bincode = "1" byteorder = "1.0" bytes = "0.4" @@ -43,7 +43,7 @@ http = "0.1" hyper = { version = "0.12", optional = true } hyperx = { version = "0.12", optional = true } jobserver = "0.1" -jsonwebtoken = { version = "5.0", optional = true } +jsonwebtoken = { version = "6.0.1", optional = true } lazy_static = "1.0.0" libc = "0.2.10" local-encoding = "0.2.0" @@ -59,7 +59,7 @@ redis = { version = "0.9.0", optional = true } regex = "1" reqwest = { version = "0.9.11", optional = true } retry = "0.4.0" -ring = "0.13.2" +ring = "0.14.6" sha-1 = { version = "0.8", optional = true } sha2 = { version = "0.8", optional = true } serde = "1.0" @@ -78,8 +78,9 @@ tower = "0.1" tokio-tcp = "0.1" tokio-timer = "0.2" toml = "0.4" -uuid = { version = "0.7", features = ["v4"] } +untrusted = { version = "0.6.0", optional = true } url = { version = "1.0", optional = true } +uuid = { version = "0.7", features = ["v4"] } walkdir = "1.0.7" which = "2" zip = { version = "0.4", default-features = false, features = ["deflate"] } @@ -97,8 +98,6 @@ version-compare = { version = "0.0.8", optional = true } [patch.crates-io] # Waiting for #151 to make it into a release tiny_http = { git = "https://github.com/tiny-http/tiny-http.git", rev = "619680de" } -# Waiting for https://github.com/Keats/jsonwebtoken/pull/74 -jsonwebtoken = { git = "https://github.com/Jake-Shadle/jsonwebtoken.git", rev = "2f469a61" } [dev-dependencies] assert_cmd = "0.9" @@ -132,7 +131,7 @@ all = ["dist-client", "redis", "s3", "memcached", "gcs", "azure"] azure = ["chrono", "hyper", "hyperx", "url", "hmac", "md-5", "sha2"] s3 = ["chrono", "hyper", "hyperx", "reqwest", "simple-s3", "hmac", "sha-1"] simple-s3 = [] -gcs = ["chrono", "hyper", "hyperx", "jsonwebtoken", "reqwest", "url"] +gcs = ["chrono", "hyper", "hyperx", "reqwest", "untrusted", "url"] memcached = ["memcached-rs"] # Enable features that require unstable features of Nightly Rust. unstable = [] diff --git a/src/bin/sccache-dist/main.rs b/src/bin/sccache-dist/main.rs index feabd1f64..52bad41eb 100644 --- a/src/bin/sccache-dist/main.rs +++ b/src/bin/sccache-dist/main.rs @@ -380,7 +380,6 @@ fn run(command: Command) -> Result { let validation = jwt::Validation { leeway: 0, validate_exp: false, - validate_iat: false, validate_nbf: false, aud: None, iss: None, diff --git a/src/cache/gcs.rs b/src/cache/gcs.rs index 963b68e5b..9ae8c9d97 100644 --- a/src/cache/gcs.rs +++ b/src/cache/gcs.rs @@ -13,24 +13,25 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cell::RefCell; -use std::fmt; -use std::io; -use std::rc::Rc; -use std::time; - -use crate::cache::{Cache, CacheRead, CacheWrite, Storage}; -use crate::jwt; -use futures::future::Shared; -use futures::{future, Async, Future, Stream}; +use std::{cell::RefCell, fmt, io, rc::Rc, time}; + +use crate::{ + cache::{Cache, CacheRead, CacheWrite, Storage}, + errors::*, + util::HeadersExt, +}; +use futures::{ + future::{self, Shared}, + Async, Future, Stream, +}; use hyper::Method; use hyperx::header::{Authorization, Bearer, ContentLength, ContentType}; use reqwest::r#async::{Client, Request}; -use url::form_urlencoded; -use url::percent_encoding::{percent_encode, PATH_SEGMENT_ENCODE_SET, QUERY_ENCODE_SET}; - -use crate::errors::*; -use crate::util::HeadersExt; +use serde::de; +use url::{ + form_urlencoded, + percent_encoding::{percent_encode, PATH_SEGMENT_ENCODE_SET, QUERY_ENCODE_SET}, +}; /// GCS bucket struct Bucket { @@ -161,28 +162,71 @@ pub enum ServiceAccountInfo { AccountKey(ServiceAccountKey), } +fn deserialize_gcp_key<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: de::Deserializer<'de>, +{ + struct Visitor; + + impl<'de> de::Visitor<'de> for Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("private key string") + } + + fn visit_str(self, v: &str) -> std::result::Result + where + E: de::Error, + { + // -----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----\n + let key_string = v + .splitn(5, "-----") + .nth(2) + .ok_or_else(|| E::custom("invalid private key format"))?; + + // Strip out all of the newlines + let key_string = key_string.split_whitespace().fold( + String::with_capacity(key_string.len()), + |mut s, line| { + s.push_str(line); + s + }, + ); + + base64::decode_config(key_string.as_bytes(), base64::STANDARD) + .map_err(|e| E::custom(format!("failed to decode from base64 string: {}", e))) + } + } + + deserializer.deserialize_any(Visitor) +} + /// ServiceAccountKey is a subset of the information in the JSON service account credentials. /// /// Note: by default, serde ignores extra fields when deserializing. This allows us to keep this /// structure minimal and not list all the fields present in a service account credential file. #[derive(Debug, Deserialize)] pub struct ServiceAccountKey { - private_key: String, + #[serde(deserialize_with = "deserialize_gcp_key")] + private_key: Vec, client_email: String, + /// The URI we send the token requests to, eg https://oauth2.googleapis.com/token + token_uri: String, } /// JwtClaims are the required claims that must be present in the OAUTH token request JWT. #[derive(Serialize)] -struct JwtClaims { +struct JwtClaims<'a> { #[serde(rename = "iss")] - issuer: String, - scope: String, + issuer: &'a str, #[serde(rename = "aud")] - audience: String, + audience: &'a str, #[serde(rename = "exp")] expiration: i64, #[serde(rename = "iat")] issued_at: i64, + scope: &'a str, } /// TokenMsg is a subset of the information provided by GCS in response to an OAUTH token request. @@ -217,6 +261,56 @@ pub struct GCSCredential { expiration_time: chrono::DateTime, } +/// A basic JWT header, the alg defaults to HS256 and typ is automatically +/// set to `JWT`. All the other fields are optional. +#[derive(Serialize)] +struct Header<'a> { + /// The type of JWS: it can only be "JWT" here + /// + /// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9). + pub typ: &'a str, + /// The algorithm used + /// + /// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1). + pub alg: &'a str, +} + +fn to_jwt_part(input: &T) -> Result { + let json = serde_json::to_string(input)?; + Ok(base64::encode_config( + json.as_bytes(), + base64::URL_SAFE_NO_PAD, + )) +} + +use ring::signature; + +fn sign_rsa( + signing_input: &str, + key: &[u8], + alg: &'static dyn signature::RsaEncoding, +) -> Result { + let key_pair = signature::RsaKeyPair::from_pkcs8(untrusted::Input::from(key)) + .chain_err(|| "failed to deserialize rsa key")?; + + let mut signature = vec![0; key_pair.public_modulus_len()]; + let rng = ring::rand::SystemRandom::new(); + key_pair + .sign(alg, &rng, signing_input.as_bytes(), &mut signature) + .chain_err(|| "failed to sign JWT claim")?; + + Ok(base64::encode_config(&signature, base64::URL_SAFE_NO_PAD)) +} + +fn encode(header: &Header<'_>, claims: &JwtClaims<'_>, key: &[u8]) -> Result { + let encoded_header = to_jwt_part(header)?; + let encoded_claims = to_jwt_part(claims)?; + let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join("."); + let signature = sign_rsa(&*signing_input, key, &signature::RSA_PKCS1_SHA256)?; + + Ok([signing_input, signature].join(".")) +} + impl GCSCredentialProvider { pub fn new(rw_mode: RWMode, sa_info: ServiceAccountInfo) -> Self { GCSCredentialProvider { @@ -231,37 +325,26 @@ impl GCSCredentialProvider { sa_key: &ServiceAccountKey, expire_at: &chrono::DateTime, ) -> Result { - let scope = (match self.rw_mode { + let scope = match self.rw_mode { RWMode::ReadOnly => "https://www.googleapis.com/auth/devstorage.readonly", RWMode::ReadWrite => "https://www.googleapis.com/auth/devstorage.read_write", - }) - .to_owned(); - - let jwt_claims = JwtClaims { - issuer: sa_key.client_email.clone(), - scope: scope, - audience: "https://www.googleapis.com/oauth2/v4/token".to_owned(), - expiration: expire_at.timestamp(), - issued_at: chrono::offset::Utc::now().timestamp(), }; - // Could also use the pem crate, but that seems overly complicated for just the specific - // case of GCP keys - let key_string = sa_key - .private_key - .splitn(5, "-----") - .nth(2) - .ok_or_else(|| "invalid key format")?; - // Skip the leading `\n` - let key_bytes = base64::decode_config(key_string[1..].as_bytes(), base64::MIME)?; - - let auth_request_jwt = jwt::encode( - &jwt::Header::new(jwt::Algorithm::RS256), - &jwt_claims, - &key_bytes, - )?; - - Ok(auth_request_jwt) + Ok(encode( + &Header { + typ: "JWT", + alg: "RS256", + }, + &JwtClaims { + issuer: &sa_key.client_email, + scope, + audience: &sa_key.token_uri, + expiration: expire_at.timestamp(), + issued_at: chrono::offset::Utc::now().timestamp(), + }, + &sa_key.private_key, + ) + .unwrap()) } fn request_new_token( @@ -272,12 +355,12 @@ impl GCSCredentialProvider { let client = client.clone(); let expires_at = chrono::offset::Utc::now() + chrono::Duration::minutes(59); let auth_jwt = self.auth_request_jwt(sa_key, &expires_at); + let url = sa_key.token_uri.clone(); // Request credentials Box::new( future::result(auth_jwt) .and_then(move |auth_jwt| { - let url = "https://www.googleapis.com/oauth2/v4/token"; let params = form_urlencoded::Serializer::new(String::new()) .append_pair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") .append_pair("assertion", &auth_jwt) @@ -312,6 +395,7 @@ impl GCSCredentialProvider { // Convert body to string and parse the token out of the response let body_str = String::from_utf8(body)?; let token_msg: TokenMsg = serde_json::from_str(&body_str)?; + Ok(GCSCredential { token: token_msg.access_token, expiration_time: expires_at, diff --git a/src/dist/http.rs b/src/dist/http.rs index 418f172ec..35ae0f87e 100644 --- a/src/dist/http.rs +++ b/src/dist/http.rs @@ -407,7 +407,6 @@ mod server { static ref JWT_VALIDATION: jwt::Validation = jwt::Validation { leeway: 0, validate_exp: false, - validate_iat: false, validate_nbf: false, aud: None, iss: None,