Skip to content

Commit 423391b

Browse files
committed
RHTAS: add/update old-style targets
Signed-off-by: Firas Ghanmi <fghanmi@redhat.com>
1 parent e588bb1 commit 423391b

File tree

3 files changed

+356
-0
lines changed

3 files changed

+356
-0
lines changed

tuftool/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,29 @@ tuftool update \
115115
--timestamp-version 2 \
116116
--outdir "${WRK}/tuf-repo" \
117117
--metadata-url file:///$WRK/tuf-repo/metadata
118+
119+
#[Optional] Set an RHTAS target (fulcio, ctlog, rekor, tsa)!
120+
touch "${WRK}/input/ctlog.crt"
121+
122+
tuftool rhtas \
123+
--root "${ROOT}" \
124+
--key "${WRK}/keys/root.pem" \
125+
--set-ctlog-target "${WRK}/input/ctlog.crt" \
126+
--ctlog-uri "https://ctfe.sigstore.dev" \
127+
--ctlog-usage "CTFE" \
128+
--targets-expires 'in 3 weeks' \
129+
--targets-version 3 \
130+
--snapshot-expires 'in 3 weeks' \
131+
--snapshot-version 3 \
132+
--timestamp-expires 'in 1 week' \
133+
--timestamp-version 3 \
134+
--outdir "${WRK}/tuf-repo" \
135+
--metadata-url file:///$WRK/tuf-repo/metadata
136+
118137
```
119138

139+
140+
120141
### Download TUF Repo
121142
Now that we have created TUF repo, we can inspect it using download command.
122143
Download command is usually used to download a remote repo using HTTP/S url, but

tuftool/src/main.rs

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ mod download_root;
2424
mod error;
2525
mod remove_key_role;
2626
mod remove_role;
27+
mod rhtas;
2728
mod root;
2829
mod source;
2930
mod transfer_metadata;
@@ -90,6 +91,8 @@ enum Command {
9091
TransferMetadata(transfer_metadata::TransferMetadataArgs),
9192
/// Update a TUF repository's metadata and optionally add targets
9293
Update(Box<update::UpdateArgs>),
94+
/// Manage RHTAS TUF
95+
Rhtas(Box<rhtas::RhtasArgs>),
9396
}
9497

9598
impl Command {
@@ -99,6 +102,7 @@ impl Command {
99102
Command::Root(root_subcommand) => root_subcommand.run().await,
100103
Command::Download(args) => args.run().await,
101104
Command::Update(args) => args.run().await,
105+
Command::Rhtas(args) => args.run().await,
102106
Command::Delegation(cmd) => cmd.run().await,
103107
Command::Clone(cmd) => cmd.run().await,
104108
Command::TransferMetadata(cmd) => cmd.run().await,

tuftool/src/rhtas.rs

+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
use crate::build_targets;
5+
use crate::common::UNUSED_URL;
6+
use crate::datetime::parse_datetime;
7+
use crate::error::{self, Result};
8+
use crate::source::parse_key_source;
9+
use chrono::{DateTime, Utc};
10+
use clap::Parser;
11+
use serde_json::json;
12+
use snafu::{OptionExt, ResultExt};
13+
use std::num::NonZeroU64;
14+
use std::path::{Path, PathBuf};
15+
use tough::editor::RepositoryEditor;
16+
use tough::{ExpirationEnforcement, RepositoryLoader};
17+
use url::Url;
18+
19+
#[derive(Debug, Parser)]
20+
pub(crate) struct RhtasArgs {
21+
/// Allow repo download for expired metadata
22+
#[arg(long)]
23+
allow_expired_repo: bool,
24+
25+
/// Follow symbolic links in the given directory when adding targets
26+
#[arg(short, long)]
27+
follow: bool,
28+
29+
/// Incoming metadata from delegatee
30+
#[arg(short, long = "incoming-metadata")]
31+
indir: Option<Url>,
32+
33+
/// Key files to sign with
34+
#[arg(short, long = "key", required = true)]
35+
keys: Vec<String>,
36+
37+
/// TUF repository metadata base URL
38+
#[arg(short, long = "metadata-url")]
39+
metadata_base_url: Url,
40+
41+
/// The directory where the updated repository will be written
42+
#[arg(short, long)]
43+
outdir: PathBuf,
44+
45+
/// Path to root.json file for the repository
46+
#[arg(short, long)]
47+
root: PathBuf,
48+
49+
/// Role of incoming metadata
50+
#[arg(long)]
51+
role: Option<String>,
52+
53+
/// Expiration of snapshot.json file; can be in full RFC 3339 format, or something like 'in
54+
/// 7 days'
55+
#[arg(long, value_parser = parse_datetime)]
56+
snapshot_expires: DateTime<Utc>,
57+
58+
/// Version of snapshot.json file
59+
#[arg(long)]
60+
snapshot_version: NonZeroU64,
61+
62+
/// Path to the new Fulcio target file to add to the targets
63+
#[arg(long = "set-fulcio-target")]
64+
fulcio_target: Option<PathBuf>,
65+
66+
/// Status for the Fulcio target
67+
#[arg(long, default_value = "Active")]
68+
fulcio_status: String,
69+
70+
/// URI for the Fulcio target
71+
#[arg(long, default_value = "https://fulcio.sigstore.dev")]
72+
fulcio_uri: String,
73+
74+
/// Usage for the Fulcio target
75+
#[arg(long, default_value = "Fulcio")]
76+
fulcio_usage: String,
77+
78+
/// Path to the new Ctlog target file
79+
#[arg(long = "set-ctlog-target")]
80+
ctlog_target: Option<PathBuf>,
81+
82+
/// Status for the Ctlog certificate
83+
#[arg(long, default_value = "Active")]
84+
ctlog_status: String,
85+
86+
/// URI for the Ctlog certificate
87+
#[arg(long, default_value = "https://ctfe.sigstore.dev/test")]
88+
ctlog_uri: String,
89+
90+
/// Usage for the Ctlog certificate
91+
#[arg(long, default_value = "CTFE")]
92+
ctlog_usage: String,
93+
94+
/// Path to the new rekor target file
95+
#[arg(long = "set-rekor-target")]
96+
rekor_target: Option<PathBuf>,
97+
98+
/// Status for the rekor certificate
99+
#[arg(long, default_value = "Active")]
100+
rekor_status: String,
101+
102+
/// URI for the rekor certificate
103+
#[arg(long, default_value = "https://rekor.sigstore.dev")]
104+
rekor_uri: String,
105+
106+
/// Usage for the rekor certificate
107+
#[arg(long, default_value = "Rekor")]
108+
rekor_usage: String,
109+
110+
/// Path to the new Timestamp Authority target file
111+
#[arg(long = "set-tsa-target")]
112+
tsa_target: Option<PathBuf>,
113+
114+
/// Status for the tsa certificate
115+
#[arg(long, default_value = "Active")]
116+
tsa_status: String,
117+
118+
/// URI for the tsa certificate
119+
#[arg(long, default_value = "")]
120+
tsa_uri: String,
121+
122+
/// Usage for the tsa certificate
123+
#[arg(long, default_value = "Timestamp Authority")]
124+
tsa_usage: String,
125+
126+
/// Expiration of targets.json file; can be in full RFC 3339 format, or something like 'in
127+
/// 7 days'
128+
#[arg(long, value_parser = parse_datetime)]
129+
targets_expires: DateTime<Utc>,
130+
131+
/// Version of targets.json file
132+
#[arg(long)]
133+
targets_version: NonZeroU64,
134+
135+
/// Expiration of timestamp.json file; can be in full RFC 3339 format, or something like 'in
136+
/// 7 days'
137+
#[arg(long, value_parser = parse_datetime)]
138+
timestamp_expires: DateTime<Utc>,
139+
140+
/// Version of timestamp.json file
141+
#[arg(long)]
142+
timestamp_version: NonZeroU64,
143+
}
144+
145+
fn expired_repo_warning<P: AsRef<Path>>(path: P) {
146+
#[rustfmt::skip]
147+
eprintln!("\
148+
=================================================================
149+
Updating repo at {}
150+
WARNING: `--allow-expired-repo` was passed; this is unsafe and will not establish trust, use only for testing!
151+
=================================================================",
152+
path.as_ref().display());
153+
}
154+
155+
impl RhtasArgs {
156+
pub(crate) async fn run(&self) -> Result<()> {
157+
let expiration_enforcement = if self.allow_expired_repo {
158+
expired_repo_warning(&self.outdir);
159+
ExpirationEnforcement::Unsafe
160+
} else {
161+
ExpirationEnforcement::Safe
162+
};
163+
let repository = RepositoryLoader::new(
164+
&tokio::fs::read(&self.root)
165+
.await
166+
.context(error::OpenRootSnafu { path: &self.root })?,
167+
self.metadata_base_url.clone(),
168+
Url::parse(UNUSED_URL).context(error::UrlParseSnafu { url: UNUSED_URL })?,
169+
)
170+
.expiration_enforcement(expiration_enforcement)
171+
.load()
172+
.await
173+
.context(error::RepoLoadSnafu)?;
174+
self.update_metadata(
175+
RepositoryEditor::from_repo(&self.root, repository)
176+
.await
177+
.context(error::EditorFromRepoSnafu { path: &self.root })?,
178+
)
179+
.await
180+
}
181+
182+
async fn update_metadata(&self, mut editor: RepositoryEditor) -> Result<()> {
183+
let mut keys = Vec::new();
184+
for source in &self.keys {
185+
let key_source = parse_key_source(source)?;
186+
keys.push(key_source);
187+
}
188+
189+
editor
190+
.targets_version(self.targets_version)
191+
.context(error::DelegationStructureSnafu)?
192+
.targets_expires(self.targets_expires)
193+
.context(error::DelegationStructureSnafu)?
194+
.snapshot_version(self.snapshot_version)
195+
.snapshot_expires(self.snapshot_expires)
196+
.timestamp_version(self.timestamp_version)
197+
.timestamp_expires(self.timestamp_expires);
198+
199+
// If the "set-fulcio-target" argument was passed, build a target
200+
// and add it to the repository.
201+
// user can specify fulcio-status, fulcio-uri & fulcio-usage
202+
self.set_fulcio_target(&mut editor).await?;
203+
204+
// If the "set-ctlog-target" argument was passed, build a target
205+
// and add it to the repository.
206+
// user can specify ctlog-status, ctlog-uri & ctlog-usage
207+
self.set_ctlog_target(&mut editor).await?;
208+
209+
// If the "set-rekor-target" argument was passed, build a target
210+
// and add it to the repository.
211+
// user can specify rekor-status, rekor-uri & rekor-usage
212+
self.set_rekor_target(&mut editor).await?;
213+
214+
// If the "set-tsa-target" argument was passed, build a target
215+
// and add it to the repository.
216+
// user can specify tsa-status, tsa-uri & tsa-usage
217+
self.set_tsa_target(&mut editor).await?;
218+
219+
// If a `Targets` metadata needs to be updated
220+
if self.role.is_some() && self.indir.is_some() {
221+
editor
222+
.sign_targets_editor(&keys)
223+
.await
224+
.context(error::DelegationStructureSnafu)?
225+
.update_delegated_targets(
226+
self.role.as_ref().context(error::MissingSnafu {
227+
what: "delegated role",
228+
})?,
229+
self.indir
230+
.as_ref()
231+
.context(error::MissingSnafu {
232+
what: "delegated role metadata url",
233+
})?
234+
.as_str(),
235+
)
236+
.await
237+
.context(error::DelegateeNotFoundSnafu {
238+
role: self.role.as_ref().unwrap().clone(),
239+
})?;
240+
}
241+
242+
// Sign the repo
243+
let signed_repo = editor.sign(&keys).await.context(error::SignRepoSnafu)?;
244+
245+
// Write the metadata to the outdir
246+
let metadata_dir = &self.outdir.join("metadata");
247+
signed_repo
248+
.write(metadata_dir)
249+
.await
250+
.context(error::WriteRepoSnafu {
251+
directory: metadata_dir,
252+
})?;
253+
254+
Ok(())
255+
}
256+
async fn set_fulcio_target(&self, editor: &mut RepositoryEditor) -> Result<()> {
257+
if let Some(ref fulcio_target_path) = self.fulcio_target {
258+
let mut fulcio_target = build_targets(fulcio_target_path, self.follow).await?;
259+
260+
let custom_sigstore_metadata = json!({
261+
"status": self.fulcio_status,
262+
"uri": self.fulcio_uri,
263+
"usage": self.fulcio_usage
264+
});
265+
266+
if let Some((target_name, target)) = fulcio_target.iter_mut().next() {
267+
target.custom.insert("sigstore".to_string(), custom_sigstore_metadata);
268+
editor.add_target(target_name.clone(), target.clone())
269+
.context(error::DelegationStructureSnafu)?;
270+
}
271+
}
272+
Ok(())
273+
}
274+
275+
async fn set_ctlog_target(&self, editor: &mut RepositoryEditor) -> Result<()> {
276+
if let Some(ref ctlog_target_path) = self.ctlog_target {
277+
let mut ctlog_target = build_targets(ctlog_target_path, self.follow).await?;
278+
279+
let custom_sigstore_metadata = json!({
280+
"status": self.ctlog_status,
281+
"uri": self.ctlog_uri,
282+
"usage": self.ctlog_usage
283+
});
284+
285+
if let Some((target_name, target)) = ctlog_target.iter_mut().next() {
286+
target.custom.insert("sigstore".to_string(), custom_sigstore_metadata);
287+
editor.add_target(target_name.clone(), target.clone())
288+
.context(error::DelegationStructureSnafu)?;
289+
}
290+
}
291+
Ok(())
292+
}
293+
294+
async fn set_rekor_target(&self, editor: &mut RepositoryEditor) -> Result<()> {
295+
if let Some(ref rekor_target_path) = self.rekor_target {
296+
let mut rekor_target = build_targets(rekor_target_path, self.follow).await?;
297+
298+
let custom_sigstore_metadata = json!({
299+
"status": self.rekor_status,
300+
"uri": self.rekor_uri,
301+
"usage": self.rekor_usage
302+
});
303+
304+
if let Some((target_name, target)) = rekor_target.iter_mut().next() {
305+
target.custom.insert("sigstore".to_string(), custom_sigstore_metadata);
306+
editor.add_target(target_name.clone(), target.clone())
307+
.context(error::DelegationStructureSnafu)?;
308+
}
309+
}
310+
Ok(())
311+
}
312+
313+
async fn set_tsa_target(&self, editor: &mut RepositoryEditor) -> Result<()> {
314+
if let Some(ref tsa_target_path) = self.tsa_target {
315+
let mut tsa_target = build_targets(tsa_target_path, self.follow).await?;
316+
317+
let custom_sigstore_metadata = json!({
318+
"status": self.tsa_status,
319+
"uri": self.tsa_uri,
320+
"usage": self.tsa_usage
321+
});
322+
323+
if let Some((target_name, target)) = tsa_target.iter_mut().next() {
324+
target.custom.insert("sigstore".to_string(), custom_sigstore_metadata);
325+
editor.add_target(target_name.clone(), target.clone())
326+
.context(error::DelegationStructureSnafu)?;
327+
}
328+
}
329+
Ok(())
330+
}
331+
}

0 commit comments

Comments
 (0)