1use std::io::Write as _;
6use std::time::Duration;
7
8use mkit_attest::grant::{Namespace, RelyingParty};
9use mkit_core::layout::RepoLayout;
10use mkit_transport_connect::ConnectTransport;
11
12use super::owner::{self, NativeOwner, OwnerArgs, Plan, SignCtx};
13use super::remote::{Driven, Target, drive, interruptible_sleep};
14use super::spec::parse_ttl;
15use crate::commands::{error, usage_error};
16use crate::config::{self, LayeredConfig};
17use crate::exit;
18use crate::remote_dispatch;
19
20const READ_TIMEOUT: Duration = Duration::from_secs(30);
22
23#[derive(Debug)]
25pub struct Ctx {
26 pub layout: RepoLayout,
27 pub layered: LayeredConfig,
28 pub relying_parties: Vec<RelyingParty>,
29}
30
31impl Ctx {
32 pub fn load() -> Result<Self, u8> {
38 let cwd =
39 std::env::current_dir().map_err(|e| error(&format!("cwd: {e}"), exit::NOINPUT))?;
40 let layout = crate::commands::resolve_layout(&cwd)?;
41 let layered = config::read_layered(&layout)
42 .map_err(|e| error(&format!("config: {e}"), exit::CONFIG_ERROR))?;
43 let relying_parties = super::parse_relying_parties(&layered.merged.grant_webauthn_rp)
44 .map_err(|e| error(&format!("grant.webauthn_rp: {e}"), exit::CONFIG_ERROR))?;
45 Ok(Self {
46 layout,
47 layered,
48 relying_parties,
49 })
50 }
51
52 pub fn plan(&self, args: &OwnerArgs, hint: Option<Namespace>) -> Result<Plan, String> {
57 let cfg = &self.layered.merged;
58 let ed25519 =
59 || NativeOwner::ed25519(remote_dispatch::owner_ed25519_signer(cfg, &self.layout)?);
60 owner::resolve(
61 args,
62 hint,
63 &SignCtx {
64 ed25519: &ed25519,
65 cfg,
66 relying_parties: &self.relying_parties,
67 },
68 )
69 }
70
71 pub fn open_unsigned(&self, target: &Target) -> Result<ConnectTransport, String> {
77 self.open(target, false)
78 }
79
80 pub fn open_signed(&self, target: &Target) -> Result<ConnectTransport, String> {
86 if !self.layered.merged.transport_auth_envelope() {
87 return Err(
88 "envelope-mode visibility signs with your mkit key: run `mkit config transport_auth envelope` and trust the remote with `mkit config trusted_remote_endpoint <url>`, or use --statement to sign with an owner key instead"
89 .to_owned(),
90 );
91 }
92 self.open(target, true)
93 }
94
95 fn open(&self, target: &Target, sign: bool) -> Result<ConnectTransport, String> {
96 remote_dispatch::open_connect_trusted(
97 &target.endpoint,
98 &target.name,
99 target.repo_chosen,
100 &self.layered,
101 &self.layout,
102 sign,
103 )
104 .map_err(|e| e.to_string())
105 }
106
107 pub fn read_epoch(tx: &ConnectTransport, namespace: &Namespace) -> Result<u64, String> {
112 let name = namespace.to_string();
113 match drive(
114 || tx.get_grant_epoch(&name),
115 READ_TIMEOUT,
116 interruptible_sleep,
117 )
118 .map_err(|e| format!("GetGrantEpoch for {name}: {e}"))?
119 {
120 Driven::Done(epoch) => Ok(epoch),
121 Driven::TimedOut { .. } => Err(format!(
122 "GetGrantEpoch for {name}: the server kept answering `unavailable`"
123 )),
124 Driven::Cancelled => Err("interrupted".to_owned()),
125 }
126 }
127}
128
129impl Ctx {
130 pub fn read_epoch_once(tx: &ConnectTransport, namespace: &Namespace) -> Result<u64, String> {
136 let name = namespace.to_string();
137 match tx
138 .get_grant_epoch_once(&name)
139 .map_err(|e| format!("GetGrantEpoch for {name}: {e}"))?
140 {
141 mkit_transport_connect::Completion::Done(epoch) => Ok(epoch),
142 mkit_transport_connect::Completion::Pending { .. } => Err(format!(
143 "GetGrantEpoch for {name}: the server answered `unavailable`"
144 )),
145 }
146 }
147}
148
149pub fn parse_timeout(text: &str) -> Result<Duration, String> {
154 let ms = parse_ttl(text)?;
155 Ok(Duration::from_millis(u64::try_from(ms).unwrap_or(0)))
156}
157
158#[must_use]
161pub fn print_statement(statement: &[u8], namespace: &Namespace) -> u8 {
162 let mut stdout = std::io::stdout().lock();
163 if stdout
164 .write_all(statement)
165 .and_then(|()| stdout.flush())
166 .is_err()
167 {
168 return error("write statement", exit::GENERAL_ERROR);
169 }
170 let mut stderr = std::io::stderr().lock();
171 let _ = write!(
172 stderr,
173 "\n{}",
174 owner::signing_instructions(statement, namespace)
175 );
176 exit::OK
177}
178
179pub fn parse_namespace(text: Option<&str>) -> Result<Option<Namespace>, u8> {
184 text.map(|t| {
185 Namespace::parse(t).map_err(|e| {
186 usage_error(&format!(
187 "--namespace `{t}`: {e} (expected ed25519-<64 hex> or 0x<40 hex>)"
188 ))
189 })
190 })
191 .transpose()
192}
193
194#[must_use]
197pub fn finish_wait<T>(outcome: &Driven<T>, what: &str) -> Option<u8> {
198 match outcome {
199 Driven::Done(_) => None,
200 Driven::TimedOut { waited } => Some(error(
201 &format!(
202 "{what} is still completing after {}s; it may yet finish on the server. Check before retrying (a new run signs a new statement)",
203 waited.as_secs()
204 ),
205 exit::TEMPFAIL,
206 )),
207 Driven::Cancelled => Some(error(
208 &format!("interrupted; {what} may still complete on the server. Check before retrying"),
209 exit::TEMPFAIL,
210 )),
211 }
212}