]>
Commit | Line | Data |
---|---|---|
fe692bf9 FG |
1 | //! Registry asymmetric authentication support. See [RFC 3231] for more. |
2 | //! | |
3 | //! [RFC 3231]: https://rust-lang.github.io/rfcs/3231-cargo-asymmetric-tokens.html | |
4 | ||
5 | use pasetors::keys::AsymmetricPublicKey; | |
6 | use pasetors::keys::AsymmetricSecretKey; | |
7 | use pasetors::paserk; | |
8 | use pasetors::paserk::FormatAsPaserk; | |
9 | use pasetors::version3; | |
10 | use pasetors::version3::PublicToken; | |
11 | use time::format_description::well_known::Rfc3339; | |
12 | use time::OffsetDateTime; | |
13 | ||
14 | use crate::core::SourceId; | |
15 | use crate::ops::RegistryCredentialConfig; | |
16 | use crate::CargoResult; | |
17 | ||
18 | use super::Mutation; | |
19 | use super::Secret; | |
20 | ||
21 | /// The main body of an asymmetric token as describe in RFC 3231. | |
22 | #[derive(serde::Serialize)] | |
23 | struct Message<'a> { | |
24 | iat: &'a str, | |
25 | #[serde(skip_serializing_if = "Option::is_none")] | |
26 | sub: Option<&'a str>, | |
27 | #[serde(skip_serializing_if = "Option::is_none")] | |
28 | mutation: Option<&'a str>, | |
29 | #[serde(skip_serializing_if = "Option::is_none")] | |
30 | name: Option<&'a str>, | |
31 | #[serde(skip_serializing_if = "Option::is_none")] | |
32 | vers: Option<&'a str>, | |
33 | #[serde(skip_serializing_if = "Option::is_none")] | |
34 | cksum: Option<&'a str>, | |
35 | #[serde(skip_serializing_if = "Option::is_none")] | |
36 | challenge: Option<&'a str>, | |
37 | /// This field is not yet used. This field can be set to a value >1 to | |
38 | /// indicate a breaking change in the token format. | |
39 | #[serde(skip_serializing_if = "Option::is_none")] | |
40 | v: Option<u8>, | |
41 | } | |
42 | ||
43 | /// The footer of an asymmetric token as describe in RFC 3231. | |
44 | #[derive(serde::Serialize)] | |
45 | struct Footer<'a> { | |
46 | url: &'a str, | |
47 | kip: paserk::Id, | |
48 | } | |
49 | ||
50 | /// Checks that a secret key is valid, and returns the associated public key in | |
51 | /// Paserk format. | |
52 | pub fn paserk_public_from_paserk_secret(secret_key: Secret<&str>) -> Option<String> { | |
53 | let secret: Secret<AsymmetricSecretKey<version3::V3>> = | |
54 | secret_key.map(|key| key.try_into()).transpose().ok()?; | |
55 | let public: AsymmetricPublicKey<version3::V3> = secret | |
56 | .as_ref() | |
57 | .map(|key| key.try_into()) | |
58 | .transpose() | |
59 | .ok()? | |
60 | .expose(); | |
61 | let mut paserk_pub_key = String::new(); | |
62 | FormatAsPaserk::fmt(&public, &mut paserk_pub_key).unwrap(); | |
63 | Some(paserk_pub_key) | |
64 | } | |
65 | ||
66 | /// Generates a public token from a registry's `credential` configuration for | |
67 | /// authenticating to a `source_id` | |
68 | /// | |
69 | /// An optional `mutation` for authenticating a mutation operation aganist the | |
70 | /// registry. | |
71 | pub fn public_token_from_credential( | |
72 | credential: RegistryCredentialConfig, | |
73 | source_id: &SourceId, | |
74 | mutation: Option<&'_ Mutation<'_>>, | |
75 | ) -> CargoResult<Secret<String>> { | |
76 | let RegistryCredentialConfig::AsymmetricKey((secret_key, secret_key_subject)) = credential else { | |
77 | anyhow::bail!("credential must be an asymmetric secret key") | |
78 | }; | |
79 | ||
80 | let secret: Secret<AsymmetricSecretKey<version3::V3>> = | |
81 | secret_key.map(|key| key.as_str().try_into()).transpose()?; | |
82 | let public: AsymmetricPublicKey<version3::V3> = secret | |
83 | .as_ref() | |
84 | .map(|key| key.try_into()) | |
85 | .transpose()? | |
86 | .expose(); | |
87 | let kip = (&public).try_into()?; | |
88 | let iat = OffsetDateTime::now_utc(); | |
89 | ||
90 | let message = Message { | |
91 | iat: &iat.format(&Rfc3339)?, | |
92 | sub: secret_key_subject.as_deref(), | |
93 | mutation: mutation.and_then(|m| { | |
94 | Some(match m { | |
95 | Mutation::PrePublish => return None, | |
96 | Mutation::Publish { .. } => "publish", | |
97 | Mutation::Yank { .. } => "yank", | |
98 | Mutation::Unyank { .. } => "unyank", | |
99 | Mutation::Owners { .. } => "owners", | |
100 | }) | |
101 | }), | |
102 | name: mutation.and_then(|m| { | |
103 | Some(match m { | |
104 | Mutation::PrePublish => return None, | |
105 | Mutation::Publish { name, .. } | |
106 | | Mutation::Yank { name, .. } | |
107 | | Mutation::Unyank { name, .. } | |
108 | | Mutation::Owners { name, .. } => *name, | |
109 | }) | |
110 | }), | |
111 | vers: mutation.and_then(|m| { | |
112 | Some(match m { | |
113 | Mutation::PrePublish | Mutation::Owners { .. } => return None, | |
114 | Mutation::Publish { vers, .. } | |
115 | | Mutation::Yank { vers, .. } | |
116 | | Mutation::Unyank { vers, .. } => *vers, | |
117 | }) | |
118 | }), | |
119 | cksum: mutation.and_then(|m| { | |
120 | Some(match m { | |
121 | Mutation::PrePublish | |
122 | | Mutation::Yank { .. } | |
123 | | Mutation::Unyank { .. } | |
124 | | Mutation::Owners { .. } => return None, | |
125 | Mutation::Publish { cksum, .. } => *cksum, | |
126 | }) | |
127 | }), | |
128 | challenge: None, // todo: PASETO with challenges | |
129 | v: None, | |
130 | }; | |
131 | ||
132 | let footer = Footer { | |
133 | url: &source_id.url().to_string(), | |
134 | kip, | |
135 | }; | |
136 | ||
137 | let secret = secret | |
138 | .map(|secret| { | |
139 | PublicToken::sign( | |
140 | &secret, | |
141 | serde_json::to_string(&message) | |
142 | .expect("cannot serialize") | |
143 | .as_bytes(), | |
144 | Some( | |
145 | serde_json::to_string(&footer) | |
146 | .expect("cannot serialize") | |
147 | .as_bytes(), | |
148 | ), | |
149 | None, | |
150 | ) | |
151 | }) | |
152 | .transpose()?; | |
153 | ||
154 | Ok(secret) | |
155 | } |