blob: 50882a74597132ebbc2abcadcd830bdaf431c0c1 [file] [log] [blame]
//! Registry asymmetric authentication support. See [RFC 3231] for more.
//!
//! [RFC 3231]: https://rust-lang.github.io/rfcs/3231-cargo-asymmetric-tokens.html
use pasetors::keys::AsymmetricPublicKey;
use pasetors::keys::AsymmetricSecretKey;
use pasetors::paserk;
use pasetors::paserk::FormatAsPaserk;
use pasetors::version3;
use pasetors::version3::PublicToken;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
use crate::core::SourceId;
use crate::ops::RegistryCredentialConfig;
use crate::CargoResult;
use super::Mutation;
use super::Secret;
/// The main body of an asymmetric token as describe in RFC 3231.
#[derive(serde::Serialize)]
struct Message<'a> {
iat: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
sub: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
mutation: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
vers: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
cksum: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
challenge: Option<&'a str>,
/// This field is not yet used. This field can be set to a value >1 to
/// indicate a breaking change in the token format.
#[serde(skip_serializing_if = "Option::is_none")]
v: Option<u8>,
}
/// The footer of an asymmetric token as describe in RFC 3231.
#[derive(serde::Serialize)]
struct Footer<'a> {
url: &'a str,
kip: paserk::Id,
}
/// Checks that a secret key is valid, and returns the associated public key in
/// Paserk format.
pub fn paserk_public_from_paserk_secret(secret_key: Secret<&str>) -> Option<String> {
let secret: Secret<AsymmetricSecretKey<version3::V3>> =
secret_key.map(|key| key.try_into()).transpose().ok()?;
let public: AsymmetricPublicKey<version3::V3> = secret
.as_ref()
.map(|key| key.try_into())
.transpose()
.ok()?
.expose();
let mut paserk_pub_key = String::new();
FormatAsPaserk::fmt(&public, &mut paserk_pub_key).unwrap();
Some(paserk_pub_key)
}
/// Generates a public token from a registry's `credential` configuration for
/// authenticating to a `source_id`
///
/// An optional `mutation` for authenticating a mutation operation aganist the
/// registry.
pub fn public_token_from_credential(
credential: RegistryCredentialConfig,
source_id: &SourceId,
mutation: Option<&'_ Mutation<'_>>,
) -> CargoResult<Secret<String>> {
let RegistryCredentialConfig::AsymmetricKey((secret_key, secret_key_subject)) = credential else {
anyhow::bail!("credential must be an asymmetric secret key")
};
let secret: Secret<AsymmetricSecretKey<version3::V3>> =
secret_key.map(|key| key.as_str().try_into()).transpose()?;
let public: AsymmetricPublicKey<version3::V3> = secret
.as_ref()
.map(|key| key.try_into())
.transpose()?
.expose();
let kip = (&public).try_into()?;
let iat = OffsetDateTime::now_utc();
let message = Message {
iat: &iat.format(&Rfc3339)?,
sub: secret_key_subject.as_deref(),
mutation: mutation.and_then(|m| {
Some(match m {
Mutation::PrePublish => return None,
Mutation::Publish { .. } => "publish",
Mutation::Yank { .. } => "yank",
Mutation::Unyank { .. } => "unyank",
Mutation::Owners { .. } => "owners",
})
}),
name: mutation.and_then(|m| {
Some(match m {
Mutation::PrePublish => return None,
Mutation::Publish { name, .. }
| Mutation::Yank { name, .. }
| Mutation::Unyank { name, .. }
| Mutation::Owners { name, .. } => *name,
})
}),
vers: mutation.and_then(|m| {
Some(match m {
Mutation::PrePublish | Mutation::Owners { .. } => return None,
Mutation::Publish { vers, .. }
| Mutation::Yank { vers, .. }
| Mutation::Unyank { vers, .. } => *vers,
})
}),
cksum: mutation.and_then(|m| {
Some(match m {
Mutation::PrePublish
| Mutation::Yank { .. }
| Mutation::Unyank { .. }
| Mutation::Owners { .. } => return None,
Mutation::Publish { cksum, .. } => *cksum,
})
}),
challenge: None, // todo: PASETO with challenges
v: None,
};
let footer = Footer {
url: &source_id.url().to_string(),
kip,
};
let secret = secret
.map(|secret| {
PublicToken::sign(
&secret,
serde_json::to_string(&message)
.expect("cannot serialize")
.as_bytes(),
Some(
serde_json::to_string(&footer)
.expect("cannot serialize")
.as_bytes(),
),
None,
)
})
.transpose()?;
Ok(secret)
}