Skip to content

Commit 6510947

Browse files
authored
Merge pull request #186 from kpcyrd/better-attestation
Better attestations
2 parents b4be10f + ccf91b4 commit 6510947

File tree

21 files changed

+665
-64
lines changed

21 files changed

+665
-64
lines changed

Cargo.lock

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/api.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::config::ConfigFile;
22
use crate::errors::*;
3-
use crate::{auth, http, PkgArtifact, PkgGroup, PkgRelease, Status};
3+
use crate::{auth, http, PkgArtifact, PkgGroup, PkgRelease, PublicKeys, Status};
44
use chrono::prelude::*;
55
use serde::{Deserialize, Serialize};
66
use std::borrow::Cow;
@@ -205,6 +205,17 @@ impl Client {
205205
Ok(attestation.to_vec())
206206
}
207207

208+
pub async fn fetch_public_keys(&self) -> Result<PublicKeys> {
209+
let keys = self
210+
.get(Cow::Borrowed("api/v0/public-keys"))
211+
.send()
212+
.await?
213+
.error_for_status()?
214+
.json()
215+
.await?;
216+
Ok(keys)
217+
}
218+
208219
pub async fn list_queue(&self, list: &ListQueue) -> Result<QueueList> {
209220
let pkgs = self
210221
.post(Cow::Borrowed("api/v0/queue/list"))

common/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub struct HttpConfig {
8585
pub bind_addr: Option<String>,
8686
pub real_ip_header: Option<String>,
8787
pub post_body_size_limit: Option<usize>,
88+
pub transparently_sign_attestations: Option<bool>,
8889
pub endpoint: Option<String>,
8990
}
9091

common/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,8 @@ impl FromStr for Status {
162162
}
163163
}
164164
}
165+
166+
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
167+
pub struct PublicKeys {
168+
pub current: Vec<String>,
169+
}

common/src/utils.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
use crate::errors::*;
2+
use std::fs::{self, OpenOptions};
3+
use std::io::prelude::*;
4+
use std::os::unix::fs::OpenOptionsExt;
5+
use std::path::Path;
6+
17
pub fn secs_to_human(duration: i64) -> String {
28
let secs = duration % 60;
39
let mins = duration / 60;
@@ -16,6 +22,29 @@ pub fn secs_to_human(duration: i64) -> String {
1622
out.join(" ")
1723
}
1824

25+
pub fn load_or_create<F: Fn() -> Result<Vec<u8>>>(path: &Path, func: F) -> Result<Vec<u8>> {
26+
let data = match OpenOptions::new()
27+
.mode(0o640)
28+
.write(true)
29+
.create_new(true)
30+
.open(path)
31+
{
32+
Ok(mut file) => {
33+
// file didn't exist yet, generate new key
34+
let data = func()?;
35+
file.write_all(&data[..])?;
36+
data
37+
}
38+
Err(_err) => {
39+
// assume the file already exists, try reading the content
40+
debug!("Loading data from file: {path:?}");
41+
fs::read(path)?
42+
}
43+
};
44+
45+
Ok(data)
46+
}
47+
1948
#[cfg(test)]
2049
mod tests {
2150
use super::*;

contrib/confs/rebuilderd.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
## If you use a reverse proxy, use this header instead of the actual connecting ip.
66
## Make sure the reverse proxy has filters in place to prevent spoofing issues.
77
#real_ip_header = "X-Real-IP"
8+
## By default, the daemon attaches a new signature to existing attestations,
9+
## if there is no signature by the current long-term private key yet.
10+
## To turn this off, change this setting to `false` explicitly.
11+
#transparently_sign_attestations = true
812
## Set a default endpoint for rebuildctl. This is especially useful for the sync timer.
913
#endpoint = "http://127.0.0.1:8484"
1014

daemon/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ assets = [
2121
[dependencies]
2222
actix-web = "4.1.0"
2323
chrono = { version = "0.4.19", features = ["serde"] }
24-
clap = { version = "4", features = ["derive"] }
24+
clap = { version = "4", features = ["derive", "env"] }
25+
data-encoding = "2"
2526
diesel = { version = "2", features = ["sqlite", "r2d2", "chrono", "i-implement-a-third-party-backend-and-opt-into-breaking-changes"] }
2627
diesel_migrations = { version = "2", features = ["sqlite"] }
2728
dirs-next = "2.0.0"
2829
dotenvy = "0.15.0"
2930
env_logger = "0.11"
31+
in-toto = "0.4.0"
3032
log = "0.4.17"
33+
pem = "3"
3134
rand = "0.9"
3235
rebuilderd-common = { version = "=0.23.0", path = "../common" }
3336
serde = { version = "1.0.137", features = ["derive"] }

daemon/src/api.rs

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::attestation::{self, Attestation};
12
use crate::auth;
23
use crate::config::Config;
34
use crate::dashboard::DashboardState;
@@ -10,9 +11,10 @@ use actix_web::http::header::{AcceptEncoding, ContentEncoding, Encoding, Header}
1011
use actix_web::{get, http, post, HttpRequest, HttpResponse, Responder};
1112
use chrono::prelude::*;
1213
use diesel::SqliteConnection;
14+
use in_toto::crypto::PrivateKey;
1315
use rebuilderd_common::api::*;
1416
use rebuilderd_common::errors::*;
15-
use rebuilderd_common::{PkgRelease, Status};
17+
use rebuilderd_common::{PkgRelease, PublicKeys, Status};
1618
use std::collections::HashSet;
1719
use std::net::IpAddr;
1820
use std::sync::{Arc, RwLock};
@@ -429,6 +431,7 @@ pub async fn ping_build(
429431
pub async fn report_build(
430432
req: HttpRequest,
431433
cfg: web::Data<Config>,
434+
privkey: web::Data<Arc<PrivateKey>>,
432435
report: web::Json<BuildReport>,
433436
pool: web::Data<Pool>,
434437
) -> web::Result<impl Responder> {
@@ -474,11 +477,17 @@ pub async fn report_build(
474477
};
475478

476479
let encoded_attestation = match &rebuild.attestation {
477-
Some(attestation) => Some(
478-
zstd_compress(attestation.as_bytes())
479-
.await
480-
.map_err(Error::from)?,
481-
),
480+
Some(attestation) => {
481+
let mut attestation = Attestation::parse(attestation.as_bytes())?;
482+
483+
// add additional signature
484+
attestation.sign(&privkey)?;
485+
486+
// compress attestation
487+
let compressed = attestation.to_compressed_bytes().await?;
488+
489+
Some(compressed)
490+
}
482491
_ => None,
483492
};
484493

@@ -541,20 +550,33 @@ pub async fn get_build_log(
541550
pub async fn get_attestation(
542551
req: HttpRequest,
543552
id: web::Path<i32>,
553+
cfg: web::Data<Config>,
554+
privkey: web::Data<Arc<PrivateKey>>,
544555
pool: web::Data<Pool>,
545556
) -> web::Result<impl Responder> {
546557
let mut connection = pool.get().map_err(Error::from)?;
547558

548-
let build = match models::Build::get_id(*id, connection.as_mut()) {
549-
Ok(build) => build,
550-
Err(_) => return Ok(not_found()),
559+
let Ok(mut build) = models::Build::get_id(*id, connection.as_mut()) else {
560+
return Ok(not_found());
551561
};
552562

553-
if let Some(attestation) = build.attestation {
554-
forward_compressed_data(req, "application/json; charset=utf-8", attestation).await
555-
} else {
556-
Ok(not_found())
563+
let Some(mut attestation) = build.attestation else {
564+
return Ok(not_found());
565+
};
566+
567+
if cfg.transparently_sign_attestations {
568+
let (bytes, has_new_signature) =
569+
attestation::compressed_attestation_sign_if_necessary(attestation, &privkey).await?;
570+
571+
if has_new_signature {
572+
build.attestation = Some(bytes.clone());
573+
build.update(connection.as_mut())?;
574+
}
575+
576+
attestation = bytes;
557577
}
578+
579+
forward_compressed_data(req, "application/json; charset=utf-8", attestation).await
558580
}
559581

560582
#[get("/api/v0/builds/{id}/diffoscope")]
@@ -596,3 +618,11 @@ pub async fn get_dashboard(
596618
let resp = state.get_response()?;
597619
Ok(HttpResponse::Ok().json(resp))
598620
}
621+
622+
#[get("/api/v0/public-keys")]
623+
pub async fn get_public_key(privkey: web::Data<Arc<PrivateKey>>) -> web::Result<impl Responder> {
624+
let pubkey = attestation::pubkey_to_pem(privkey.public())?;
625+
Ok(HttpResponse::Ok().json(PublicKeys {
626+
current: vec![pubkey],
627+
}))
628+
}

daemon/src/args.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use clap::{ArgAction, Parser};
2+
use std::path::PathBuf;
3+
4+
#[derive(Debug, Parser)]
5+
#[command(version)]
6+
pub struct Args {
7+
/// Verbose logging
8+
#[arg(short, long, action(ArgAction::Count))]
9+
pub verbose: u8,
10+
/// Load and print a config
11+
#[arg(long, group = "action")]
12+
pub check_config: bool,
13+
/// Generate a signing keypair (this usually happens automatically)
14+
#[arg(long, group = "action")]
15+
pub keygen: bool,
16+
/// Derive the public key from a private key file
17+
#[arg(long, group = "action")]
18+
pub derive_pubkey: Option<PathBuf>,
19+
/// Configuration file path
20+
#[arg(short, long)]
21+
pub config: Option<PathBuf>,
22+
/// Long-term key used to sign attestations
23+
#[arg(
24+
long,
25+
env = "REBUILDERD_SIGNING_KEY",
26+
default_value = "./rebuilderd.sign.key"
27+
)]
28+
pub signing_key: PathBuf,
29+
}

0 commit comments

Comments
 (0)