1use anyhow::Context as _;
4use cargo_credential::{
5 Action, CacheControl, Credential, CredentialResponse, Error, Operation, RegistryInfo, Secret,
6};
7use clap::Command;
8use pasetors::{
9 keys::{AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate},
10 paserk::FormatAsPaserk,
11};
12use time::{format_description::well_known::Rfc3339, OffsetDateTime};
13use url::Url;
14
15use crate::{
16 core::SourceId,
17 ops::RegistryCredentialConfig,
18 util::{auth::registry_credential_config_raw, command_prelude::opt, context},
19 GlobalContext,
20};
21
22#[derive(serde::Serialize)]
24struct Message<'a> {
25 iat: &'a str,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 sub: Option<&'a str>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 mutation: Option<&'a str>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 name: Option<&'a str>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 vers: Option<&'a str>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 cksum: Option<&'a str>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 challenge: Option<&'a str>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 v: Option<u8>,
41}
42#[derive(serde::Serialize)]
44struct Footer<'a> {
45 url: &'a str,
46 kip: pasetors::paserk::Id,
47}
48
49pub(crate) struct PasetoCredential<'a> {
50 gctx: &'a GlobalContext,
51}
52
53impl<'a> PasetoCredential<'a> {
54 pub fn new(gctx: &'a GlobalContext) -> Self {
55 Self { gctx }
56 }
57}
58
59impl<'a> Credential for PasetoCredential<'a> {
60 fn perform(
61 &self,
62 registry: &RegistryInfo<'_>,
63 action: &Action<'_>,
64 args: &[&str],
65 ) -> Result<CredentialResponse, Error> {
66 let index_url = Url::parse(registry.index_url).context("parsing index url")?;
67 let sid = if let Some(name) = registry.name {
68 SourceId::for_alt_registry(&index_url, name)
69 } else {
70 SourceId::for_registry(&index_url)
71 }?;
72
73 let reg_cfg = registry_credential_config_raw(self.gctx, &sid)?;
74
75 let matches = Command::new("cargo:paseto")
76 .no_binary_name(true)
77 .arg(opt("key-subject", "Set the key subject for this registry").value_name("SUBJECT"))
78 .try_get_matches_from(args)
79 .map_err(Box::new)?;
80 let key_subject = matches.get_one("key-subject").map(String::as_str);
81
82 match action {
83 Action::Get(operation) => {
84 let Some(reg_cfg) = reg_cfg else {
85 return Err(Error::NotFound);
86 };
87 let Some(secret_key) = reg_cfg.secret_key.as_ref() else {
88 return Err(Error::NotFound);
89 };
90
91 let secret_key_subject = reg_cfg.secret_key_subject;
92 let secret: Secret<AsymmetricSecretKey<pasetors::version3::V3>> = secret_key
93 .val
94 .as_ref()
95 .map(|key| key.as_str().try_into())
96 .transpose()
97 .context("failed to load private key")?;
98 let public: AsymmetricPublicKey<pasetors::version3::V3> = secret
99 .as_ref()
100 .map(|key| key.try_into())
101 .transpose()
102 .context("failed to load public key from private key")?
103 .expose();
104 let kip: pasetors::paserk::Id = (&public).into();
105
106 let iat = OffsetDateTime::now_utc();
107
108 let message = Message {
109 iat: &iat.format(&Rfc3339).unwrap(),
110 sub: secret_key_subject.as_deref(),
111 mutation: match operation {
112 Operation::Publish { .. } => Some("publish"),
113 Operation::Yank { .. } => Some("yank"),
114 Operation::Unyank { .. } => Some("unyank"),
115 Operation::Owners { .. } => Some("owners"),
116 _ => None,
117 },
118 name: match operation {
119 Operation::Publish { name, .. }
120 | Operation::Yank { name, .. }
121 | Operation::Unyank { name, .. }
122 | Operation::Owners { name, .. } => Some(name),
123 _ => None,
124 },
125 vers: match operation {
126 Operation::Publish { vers, .. }
127 | Operation::Yank { vers, .. }
128 | Operation::Unyank { vers, .. } => Some(vers),
129 _ => None,
130 },
131 cksum: match operation {
132 Operation::Publish { cksum, .. } => Some(cksum),
133 _ => None,
134 },
135 challenge: None, v: None,
137 };
138 let footer = Footer {
139 url: ®istry.index_url,
140 kip,
141 };
142
143 let cache = match operation {
145 Operation::Read => CacheControl::Session,
146 _ => CacheControl::Never,
147 };
148
149 let token = secret
150 .map(|secret| {
151 pasetors::version3::PublicToken::sign(
152 &secret,
153 serde_json::to_string(&message)
154 .expect("cannot serialize")
155 .as_bytes(),
156 Some(
157 serde_json::to_string(&footer)
158 .expect("cannot serialize")
159 .as_bytes(),
160 ),
161 None,
162 )
163 })
164 .transpose()
165 .context("failed to sign request")?;
166
167 Ok(CredentialResponse::Get {
168 token,
169 cache,
170 operation_independent: false,
171 })
172 }
173 Action::Login(options) => {
174 let old_key_subject = reg_cfg.and_then(|cfg| cfg.secret_key_subject);
175 let new_token;
176 let secret_key: Secret<String>;
177 if let Some(key) = &options.token {
178 secret_key = key.clone().map(str::to_string);
179 } else {
180 let kp = AsymmetricKeyPair::<pasetors::version3::V3>::generate().unwrap();
181 secret_key = Secret::default().map(|mut key| {
182 FormatAsPaserk::fmt(&kp.secret, &mut key).unwrap();
183 key
184 });
185 }
186
187 if let Some(p) = paserk_public_from_paserk_secret(secret_key.as_deref()) {
188 eprintln!("{}", &p);
189 } else {
190 return Err("not a validly formatted PASERK secret key".into());
191 }
192 new_token = RegistryCredentialConfig::AsymmetricKey((
193 secret_key,
194 match key_subject {
195 Some(key_subject) => Some(key_subject.to_string()),
196 None => old_key_subject,
197 },
198 ));
199 context::save_credentials(self.gctx, Some(new_token), &sid)?;
200 Ok(CredentialResponse::Login)
201 }
202 Action::Logout => {
203 if reg_cfg.and_then(|c| c.secret_key).is_some() {
204 context::save_credentials(self.gctx, None, &sid)?;
205 let reg_name = sid.display_registry_name();
206 let _ = self.gctx.shell().status(
207 "Logout",
208 format!("secret-key for `{reg_name}` has been removed from local storage"),
209 );
210 Ok(CredentialResponse::Logout)
211 } else {
212 Err(Error::NotFound)
213 }
214 }
215 _ => Err(Error::OperationNotSupported),
216 }
217 }
218}
219
220pub(crate) fn paserk_public_from_paserk_secret(secret_key: Secret<&str>) -> Option<String> {
222 let secret: Secret<AsymmetricSecretKey<pasetors::version3::V3>> =
223 secret_key.map(|key| key.try_into()).transpose().ok()?;
224 let public: AsymmetricPublicKey<pasetors::version3::V3> = secret
225 .as_ref()
226 .map(|key| key.try_into())
227 .transpose()
228 .ok()?
229 .expose();
230 let mut paserk_pub_key = String::new();
231 FormatAsPaserk::fmt(&public, &mut paserk_pub_key).unwrap();
232 Some(paserk_pub_key)
233}