Skip to content
This repository was archived by the owner on Mar 22, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Replaced protobuf dependency with prost to avoid a security vulnerability.

## v3.2.0 - 2025-03-07

- Updated protobuf to 3.7.1 and regenerated code from .proto files
Expand Down
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ lazy_static = "1.4"
regex = "1.3"
# Byte collections
bytes = "1"
# Protobuf runtime
protobuf = "=3.7.1"
# gzip (de)compression
flate2 = "1.0"
# RSA signature and SHA256 checksum verification
Expand All @@ -40,6 +38,8 @@ pubgrub = "0.2"
http-auth-basic = "0.3"
# base16 encoding
base16 = { version = "0.2", features = ["alloc"] }
# Protobuf runtime
prost = "0.13.5"

[dev-dependencies]
# HTTP client
Expand All @@ -52,5 +52,5 @@ tokio = { version = "1", features = ["full"] }
toml = "0.8"

[build-dependencies]
# Protobuf code generation
protobuf-codegen = "=3.7.1"
# Protobuf codegen
prost-build = "0.13.5"
30 changes: 21 additions & 9 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
/// `prost_build` generates files in the output directory, which means that if we want
/// to use it, we would need the protoc compiler as a build dependency.
/// To get around this, we need run the build script and manually copy the generated files
/// into the `src` folder. To do so, uncomment the below lines, then copy the files from the
/// output directory into the `src/proto` folder. The path to the output directory
/// will be printed in the terminal.
///
fn main() {
// protobuf_codegen::Codegen::new()
// .protoc()
// .include("proto")
// .input("proto/signed.proto")
// .input("proto/package.proto")
// .input("proto/versions.proto")
// .out_dir("src/proto")
// .run()
// .expect("Failed to generate protobuf code from .proto files.");
// prost_build::compile_protos(
// &[
// "proto/signed.proto",
// "proto/package.proto",
// "proto/versions.proto",
// ],
// &["proto/"],
// )
// .expect("Failed to generate prost code from .proto files");

// println!(
// "cargo::warning=Regenerated proto files, which must be manually copied into the `src` directory. Generated files can be found in {}",
// std::env::var("OUT_DIR").unwrap_or_default()
// );
}
2 changes: 2 additions & 0 deletions proto/names.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
syntax = "proto2";

package names;

message Names {
// All packages in the repository
repeated Package packages = 1;
Expand Down
2 changes: 2 additions & 0 deletions proto/package.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
syntax = "proto2";

package package;

message Package {
// All releases of the package
repeated Release releases = 1;
Expand Down
2 changes: 2 additions & 0 deletions proto/signed.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
syntax = "proto2";

package signed;

message Signed {
// Signed contents
required bytes payload = 1;
Expand Down
2 changes: 2 additions & 0 deletions proto/versions.proto
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
syntax = "proto2";

package versions;

message Versions {
// All packages in the repository
repeated VersionsPackage packages = 1;
Expand Down
78 changes: 38 additions & 40 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use bytes::buf::Buf;
use flate2::read::GzDecoder;
use http::{Method, StatusCode};
use lazy_static::lazy_static;
use protobuf::{Message, MessageField};
use prost::Message;
use regex::Regex;
use ring::digest::{Context, SHA256};
use serde::Deserialize;
Expand All @@ -19,7 +19,7 @@ use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
fmt::Display,
io::BufReader,
io::{BufReader, Read},
};
use thiserror::Error;
use version::{Range, Version};
Expand Down Expand Up @@ -270,16 +270,19 @@ pub fn get_repository_versions_response(
status => return Err(ApiError::unexpected_response(status, body)),
};

let mut body = GzDecoder::new(body.reader());
let signed = Signed::parse_from_reader(&mut body)?;
let mut decoder = GzDecoder::new(body.reader());
let mut body = Vec::new();
decoder.read_to_end(&mut body)?;

let signed = Signed::decode(body.as_slice())?;

let payload =
verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?;

let versions = Versions::parse_from_bytes(&payload)?
let versions = Versions::decode(payload.as_slice())?
.packages
.into_iter()
.map(|mut n| {
.map(|n| {
let parse_version = |v: &str| {
let err = |_| ApiError::InvalidVersionFormat(v.to_string());
Version::parse(v).map_err(err)
Expand All @@ -289,7 +292,7 @@ pub fn get_repository_versions_response(
.iter()
.map(|v| parse_version(v.as_str()))
.collect::<Result<Vec<Version>, ApiError>>()?;
Ok((n.take_name(), versions))
Ok((n.name, versions))
})
.collect::<Result<HashMap<_, _>, ApiError>>()?;

Expand Down Expand Up @@ -332,22 +335,25 @@ pub fn get_package_response(
}
};

let mut body = GzDecoder::new(body.reader());
let signed = Signed::parse_from_reader(&mut body)?;
let mut decoder = GzDecoder::new(body.reader());
let mut body = Vec::new();
decoder.read_to_end(&mut body)?;

let signed = Signed::decode(body.as_slice())?;

let payload =
verify_payload(signed, public_key).map_err(|_| ApiError::IncorrectPayloadSignature)?;

let mut package = proto::package::Package::parse_from_bytes(&payload)?;
let package = proto::package::Package::decode(payload.as_slice())?;
let releases = package
.releases
.clone()
.into_iter()
.map(proto_to_release)
.collect::<Result<Vec<_>, _>>()?;
let package = Package {
name: package.take_name(),
repository: package.take_repository(),
name: package.name,
repository: package.repository,
releases,
};

Expand Down Expand Up @@ -695,7 +701,7 @@ pub enum ApiError {
IncorrectPayloadSignature,

#[error(transparent)]
InvalidProtobuf(#[from] protobuf::Error),
InvalidProtobuf(#[from] prost::DecodeError),

#[error("unexpected version format {0}")]
InvalidVersionFormat(String),
Expand Down Expand Up @@ -762,60 +768,52 @@ fn read_and_check_body(reader: impl std::io::Read, checksum: &[u8]) -> Result<Ve
}

fn proto_to_retirement_status(
status: MessageField<proto::package::RetirementStatus>,
status: Option<proto::package::RetirementStatus>,
) -> Option<RetirementStatus> {
status.into_option().map(|mut stat| RetirementStatus {
message: stat.take_message(),
status.map(|stat| RetirementStatus {
message: stat.message().into(),
reason: proto_to_retirement_reason(stat.reason()),
})
}

fn proto_to_retirement_reason(reason: proto::package::RetirementReason) -> RetirementReason {
use proto::package::RetirementReason::*;
match reason {
RETIRED_OTHER => RetirementReason::Other,
RETIRED_INVALID => RetirementReason::Invalid,
RETIRED_SECURITY => RetirementReason::Security,
RETIRED_DEPRECATED => RetirementReason::Deprecated,
RETIRED_RENAMED => RetirementReason::Renamed,
RetiredOther => RetirementReason::Other,
RetiredInvalid => RetirementReason::Invalid,
RetiredSecurity => RetirementReason::Security,
RetiredDeprecated => RetirementReason::Deprecated,
RetiredRenamed => RetirementReason::Renamed,
}
}

fn proto_to_dep(mut dep: proto::package::Dependency) -> Result<(String, Dependency), ApiError> {
let app = if dep.has_app() {
Some(dep.take_app())
} else {
None
};
let repository = if dep.has_repository() {
Some(dep.take_repository())
} else {
None
};
let requirement = Range::new(dep.take_requirement());
fn proto_to_dep(dep: proto::package::Dependency) -> Result<(String, Dependency), ApiError> {
let app = dep.app;
let repository = dep.repository;
let requirement = Range::new(dep.requirement);
Ok((
dep.take_package(),
dep.package,
Dependency {
requirement,
optional: dep.has_optional(),
optional: dep.optional.is_some(),
app,
repository,
},
))
}

fn proto_to_release(mut release: proto::package::Release) -> Result<Release<()>, ApiError> {
fn proto_to_release(release: proto::package::Release) -> Result<Release<()>, ApiError> {
let dependencies = release
.dependencies
.clone()
.into_iter()
.map(proto_to_dep)
.collect::<Result<HashMap<_, _>, _>>()?;
let version =
Version::try_from(release.version()).expect("Failed to parse version format from Hex");
let version = Version::try_from(release.version.as_str())
.expect("Failed to parse version format from Hex");
Ok(Release {
version,
outer_checksum: release.take_outer_checksum(),
outer_checksum: release.outer_checksum.unwrap_or_default(),
retirement_status: proto_to_retirement_status(release.retired),
requirements: dependencies,
meta: (),
Expand Down Expand Up @@ -980,7 +978,7 @@ fn verify_payload(mut signed: Signed, pem_public_key: &[u8]) -> Result<Vec<u8>,
.map_err(|_| ApiError::IncorrectPayloadSignature)?;
let (_, spki) = x509_parser::prelude::SubjectPublicKeyInfo::from_der(&pem.contents)
.map_err(|_| ApiError::IncorrectPayloadSignature)?;
let payload = signed.take_payload();
let payload = std::mem::take(&mut signed.payload);
let verification = ring::signature::UnparsedPublicKey::new(
&ring::signature::RSA_PKCS1_2048_8192_SHA512,
&spki.subject_public_key,
Expand Down
2 changes: 1 addition & 1 deletion src/proto/mod.rs → src/proto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @generated
#![allow(clippy::enum_variant_names)]

pub mod package;
pub mod signed;
Expand Down
Loading