Skip to content

Commit 597953e

Browse files
feat: implement pgp signature validation
Uses the pgp crate to validate signatures on downloaded artifacts when they are available and warns if those are not valid. Ref #2028
1 parent 3929244 commit 597953e

18 files changed

+831
-20
lines changed

Cargo.lock

+540
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ toml = "0.5"
5454
url = "1"
5555
wait-timeout = "0.2"
5656
xz2 = "0.1.3"
57+
pgp = { version = "0.3.1", default-features = false}
5758

5859
[dependencies.retry]
5960
version = "0.5"

src/config.rs

+28-17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::process::Command;
77
use std::str::FromStr;
88
use std::sync::Arc;
99

10+
use pgp::{Deserializable, SignedPublicKey};
11+
1012
use crate::dist::{dist, temp};
1113
use crate::errors::*;
1214
use crate::notifications::*;
@@ -33,21 +35,24 @@ impl Display for OverrideReason {
3335
}
3436
}
3537

38+
lazy_static::lazy_static! {
39+
static ref BUILTIN_PGP_KEY: SignedPublicKey = pgp::SignedPublicKey::from_armor_single(
40+
io::Cursor::new(&include_bytes!("rust-key.pgp.ascii")[..])
41+
).unwrap().0;
42+
}
43+
3644
#[derive(Debug)]
3745
pub enum PgpPublicKey {
38-
Builtin(&'static [u8]),
39-
FromEnvironment(PathBuf, Vec<u8>),
40-
FromConfiguration(PathBuf, Vec<u8>),
46+
Builtin,
47+
FromEnvironment(PathBuf, SignedPublicKey),
48+
FromConfiguration(PathBuf, SignedPublicKey),
4149
}
4250

4351
impl PgpPublicKey {
44-
/// Retrieve the key data for this key
45-
///
46-
/// This key might be ASCII Armored or may not, we make no
47-
/// guarantees.
48-
pub fn key_data(&self) -> &[u8] {
52+
/// Retrieve the key.
53+
pub fn key(&self) -> &SignedPublicKey {
4954
match self {
50-
Self::Builtin(k) => k,
55+
Self::Builtin => &*BUILTIN_PGP_KEY,
5156
Self::FromEnvironment(_, k) => &k,
5257
Self::FromConfiguration(_, k) => &k,
5358
}
@@ -57,7 +62,7 @@ impl PgpPublicKey {
5762
impl Display for PgpPublicKey {
5863
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5964
match self {
60-
Self::Builtin(_) => write!(f, "builtin Rust release key"),
65+
Self::Builtin => write!(f, "builtin Rust release key"),
6166
Self::FromEnvironment(p, _) => {
6267
write!(f, "key specified in RUST_PGP_KEY ({})", p.display())
6368
}
@@ -98,18 +103,24 @@ impl Cfg {
98103
let download_dir = rustup_dir.join("downloads");
99104

100105
// PGP keys
101-
let mut pgp_keys: Vec<PgpPublicKey> =
102-
vec![PgpPublicKey::Builtin(include_bytes!("rust-key.pgp.ascii"))];
103-
if let Some(s_path) = env::var_os("RUSTUP_PGP_KEY") {
106+
let mut pgp_keys: Vec<PgpPublicKey> = vec![PgpPublicKey::Builtin];
107+
108+
if let Some(ref s_path) = env::var_os("RUSTUP_PGP_KEY") {
104109
let path = PathBuf::from(s_path);
105-
let content = utils::read_file_bytes("RUSTUP_PGP_KEY", &path)?;
106-
pgp_keys.push(PgpPublicKey::FromEnvironment(path, content));
110+
let file = utils::open_file("RUSTUP_PGP_KEY", &path)?;
111+
let (key, _) = SignedPublicKey::from_armor_single(file)
112+
.map_err(|error| ErrorKind::InvalidPgpKey(PathBuf::from(s_path), error))?;
113+
114+
pgp_keys.push(PgpPublicKey::FromEnvironment(path, key));
107115
}
108116
settings_file.with(|s| {
109117
if let Some(s) = &s.pgp_keys {
110118
let path = PathBuf::from(s);
111-
let content = utils::read_file_bytes("PGP Key from config", &path)?;
112-
pgp_keys.push(PgpPublicKey::FromConfiguration(path, content));
119+
let file = utils::open_file("PGP Key from config", &path)?;
120+
let (key, _) = SignedPublicKey::from_armor_single(file)
121+
.map_err(|error| ErrorKind::InvalidPgpKey(PathBuf::from(s), error))?;
122+
123+
pgp_keys.push(PgpPublicKey::FromConfiguration(path, key));
113124
}
114125
Ok(())
115126
})?;

src/dist/dist.rs

+1
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,7 @@ fn try_update_from_dist_<'a>(
844844
update_hash,
845845
&download.temp_cfg,
846846
&download.notify_handler,
847+
&download.pgp_keys,
847848
) {
848849
Ok(None) => Ok(None),
849850
Ok(Some(hash)) => Ok(Some(hash)),

src/dist/download.rs

+51-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use crate::config::PgpPublicKey;
12
use crate::dist::notifications::*;
23
use crate::dist::temp;
34
use crate::errors::*;
45
use crate::utils::utils;
6+
57
use sha2::{Digest, Sha256};
68
use url::Url;
79

@@ -17,6 +19,7 @@ pub struct DownloadCfg<'a> {
1719
pub temp_cfg: &'a temp::Cfg,
1820
pub download_dir: &'a PathBuf,
1921
pub notify_handler: &'a dyn Fn(Notification<'_>),
22+
pub pgp_keys: &'a [PgpPublicKey],
2023
}
2124

2225
pub struct File {
@@ -119,9 +122,50 @@ impl<'a> DownloadCfg<'a> {
119122
Ok(utils::read_file("hash", &hash_file).map(|s| s[0..64].to_owned())?)
120123
}
121124

125+
fn download_signature(&self, url: &str) -> Result<String> {
126+
let sig_url = utils::parse_url(&(url.to_owned() + ".asc"))?;
127+
let sig_file = self.temp_cfg.new_file()?;
128+
129+
utils::download_file(&sig_url, &sig_file, None, &|n| {
130+
(self.notify_handler)(n.into())
131+
})?;
132+
133+
Ok(utils::read_file("signature", &sig_file).map(|s| s.to_owned())?)
134+
}
135+
136+
fn check_signature(&self, url: &str, file: &temp::File<'_>) -> Result<()> {
137+
assert!(
138+
!self.pgp_keys.is_empty(),
139+
"At least the builtin key must be present"
140+
);
141+
142+
let signature = self.download_signature(url).map_err(|e| {
143+
e.chain_err(|| ErrorKind::SignatureVerificationFailed {
144+
url: url.to_owned(),
145+
})
146+
})?;
147+
148+
let file_path: &Path = &file;
149+
let content = std::fs::File::open(file_path).chain_err(|| ErrorKind::ReadingFile {
150+
name: "channel data",
151+
path: PathBuf::from(file_path),
152+
})?;
153+
154+
if !crate::dist::signatures::verify_signature(content, &signature, &self.pgp_keys)? {
155+
Err(ErrorKind::SignatureVerificationFailed {
156+
url: url.to_owned(),
157+
}
158+
.into())
159+
} else {
160+
Ok(())
161+
}
162+
}
163+
122164
/// Downloads a file, sourcing its hash from the same url with a `.sha256` suffix.
123165
/// If `update_hash` is present, then that will be compared to the downloaded hash,
124166
/// and if they match, the download is skipped.
167+
/// Verifies the signature found at the same url with a `.asc` suffix, and prints a
168+
/// warning when the signature does not verify, or is not found.
125169
pub fn download_and_check(
126170
&self,
127171
url_str: &str,
@@ -167,7 +211,13 @@ impl<'a> DownloadCfg<'a> {
167211
(self.notify_handler)(Notification::ChecksumValid(url_str));
168212
}
169213

170-
// TODO: Check the signature of the file
214+
// No signatures for tarballs for now.
215+
if !url_str.ends_with(".tar.gz")
216+
&& !url_str.ends_with(".tar.xz")
217+
&& self.check_signature(&url_str, &file).is_err()
218+
{
219+
(self.notify_handler)(Notification::SignatureInvalid(url_str));
220+
}
171221

172222
Ok(Some((file, partial_hash)))
173223
}

src/dist/manifestation.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Maintains a Rust installation by installing individual Rust
22
//! platform components from a distribution server.
33
4+
use crate::config::PgpPublicKey;
45
use crate::dist::component::{Components, Package, TarGzPackage, TarXzPackage, Transaction};
56
use crate::dist::config::Config;
67
use crate::dist::dist::{Profile, TargetTriple, DEFAULT_DIST_SERVER};
@@ -351,6 +352,7 @@ impl Manifestation {
351352
update_hash: Option<&Path>,
352353
temp_cfg: &temp::Cfg,
353354
notify_handler: &dyn Fn(Notification<'_>),
355+
pgp_keys: &[PgpPublicKey],
354356
) -> Result<Option<String>> {
355357
// If there's already a v2 installation then something has gone wrong
356358
if self.read_config()?.is_some() {
@@ -388,6 +390,7 @@ impl Manifestation {
388390
download_dir: &dld_dir,
389391
temp_cfg,
390392
notify_handler,
393+
pgp_keys,
391394
};
392395

393396
let dl = dlcfg.download_and_check(&url, update_hash, ".tar.gz")?;

src/dist/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ pub mod manifest;
1313
pub mod manifestation;
1414
pub mod notifications;
1515
pub mod prefix;
16+
pub mod signatures;

src/dist/notifications.rs

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub enum Notification<'a> {
3535
ManifestChecksumFailedHack,
3636
ComponentUnavailable(&'a str, Option<&'a TargetTriple>),
3737
StrayHash(&'a Path),
38+
SignatureInvalid(&'a str),
3839
}
3940

4041
impl<'a> From<crate::utils::Notification<'a>> for Notification<'a> {
@@ -79,6 +80,7 @@ impl<'a> Notification<'a> {
7980
| ForcingUnavailableComponent(_)
8081
| StrayHash(_) => NotificationLevel::Warn,
8182
NonFatalError(_) => NotificationLevel::Error,
83+
SignatureInvalid(_) => NotificationLevel::Warn,
8284
}
8385
}
8486
}
@@ -171,6 +173,7 @@ impl<'a> Display for Notification<'a> {
171173
ForcingUnavailableComponent(component) => {
172174
write!(f, "Force-skipping unavailable component '{}'", component)
173175
}
176+
SignatureInvalid(url) => write!(f, "Signature verification failed for '{}'", url),
174177
}
175178
}
176179
}

src/dist/signatures.rs

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Signature verification support for Rustup.
2+
3+
// TODO: Determine whether we want external keyring support
4+
5+
use pgp::types::KeyTrait;
6+
use pgp::{Deserializable, StandaloneSignature};
7+
8+
use crate::config::PgpPublicKey;
9+
use crate::errors::*;
10+
11+
use std::io::Read;
12+
13+
fn squish_internal_err<E: std::fmt::Display>(err: E) -> Error {
14+
ErrorKind::SignatureVerificationInternalError(format!("{}", err)).into()
15+
}
16+
17+
pub fn verify_signature<T: Read>(
18+
mut content: T,
19+
signature: &str,
20+
keys: &[PgpPublicKey],
21+
) -> Result<bool> {
22+
let mut content_buf = Vec::new();
23+
content.read_to_end(&mut content_buf)?;
24+
let (signatures, _) =
25+
StandaloneSignature::from_string_many(signature).map_err(squish_internal_err)?;
26+
27+
for signature in signatures {
28+
let signature = signature.map_err(squish_internal_err)?;
29+
30+
for key in keys {
31+
let actual_key = key.key();
32+
if actual_key.is_signing_key() && signature.verify(actual_key, &content_buf).is_ok() {
33+
return Ok(true);
34+
}
35+
36+
for sub_key in &actual_key.public_subkeys {
37+
if sub_key.is_signing_key() && signature.verify(sub_key, &content_buf).is_ok() {
38+
return Ok(true);
39+
}
40+
}
41+
}
42+
}
43+
44+
Ok(false)
45+
}

src/errors.rs

+14
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ error_chain! {
208208
expected,
209209
calculated)
210210
}
211+
SignatureVerificationInternalError(msg: String) {
212+
description("internal error verifying signature")
213+
display("internal error verifying signature: {}", msg)
214+
}
215+
SignatureVerificationFailed {
216+
url: String,
217+
} {
218+
description("signature verification failed")
219+
display("signature verification failed for {}", url)
220+
}
211221
ComponentConflict {
212222
name: String,
213223
path: PathBuf,
@@ -348,6 +358,10 @@ error_chain! {
348358
description("bad path in tar")
349359
display("tar path '{}' is not supported", v.display())
350360
}
361+
InvalidPgpKey(v: PathBuf, error: pgp::errors::Error) {
362+
description("invalid PGP key"),
363+
display("unable to read the PGP key '{}'", v.display())
364+
}
351365
}
352366
}
353367

src/toolchain.rs

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ impl<'a> Toolchain<'a> {
159159
temp_cfg: &self.cfg.temp_cfg,
160160
download_dir: &self.cfg.download_dir,
161161
notify_handler: &*self.dist_handler,
162+
pgp_keys: self.cfg.get_pgp_keys(),
162163
}
163164
}
164165

src/utils/utils.rs

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ where
3737
})
3838
}
3939

40+
pub fn open_file(name: &'static str, path: &Path) -> Result<fs::File> {
41+
fs::File::open(path).chain_err(|| ErrorKind::ReadingFile {
42+
name,
43+
path: PathBuf::from(path),
44+
})
45+
}
46+
4047
pub fn read_file_bytes(name: &'static str, path: &Path) -> Result<Vec<u8>> {
4148
fs::read(path).chain_err(|| ErrorKind::ReadingFile {
4249
name,

tests/cli-v2.rs

+34
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,12 @@ fn make_component_unavailable(config: &Config, name: &str, target: &TargetTriple
979979
let hash_path = manifest_path.with_extension("toml.sha256");
980980
println!("{}", hash_path.display());
981981
create_hash(&manifest_path, &hash_path);
982+
983+
// update that signature
984+
use crate::mock::dist::{create_signature, write_file};
985+
let signature = create_signature(manifest_str.as_bytes()).unwrap();
986+
let sig_path = manifest_path.with_extension("toml.asc");
987+
write_file(&sig_path, &signature);
982988
}
983989

984990
#[test]
@@ -1255,3 +1261,31 @@ info: installing component 'rustc'
12551261
);
12561262
});
12571263
}
1264+
1265+
/// Invalidates the signature on the manifest of the nigthly channel.
1266+
fn make_signature_invalid(config: &Config) {
1267+
let manifest_path = config.distdir.join("dist/channel-rust-nightly.toml");
1268+
1269+
// Set signature to sth bogus.
1270+
use crate::mock::dist::{create_signature, write_file};
1271+
let signature = create_signature(b"hello invalid").unwrap();
1272+
let sig_path = manifest_path.with_extension("toml.asc");
1273+
write_file(&sig_path, &signature);
1274+
}
1275+
1276+
#[test]
1277+
fn warn_on_invalid_signature() {
1278+
setup(&|config| {
1279+
make_signature_invalid(config);
1280+
let manifest_path = config.distdir.join("dist/channel-rust-nightly.toml");
1281+
1282+
expect_stderr_ok(
1283+
config,
1284+
&["rustup", "update", "nightly", "--no-self-update"],
1285+
&format!(
1286+
"warning: Signature verification failed for 'file://{}'",
1287+
manifest_path.display()
1288+
),
1289+
);
1290+
});
1291+
}

0 commit comments

Comments
 (0)