sh4d0wup 0.11.0

Signing-key abuse and update exploitation framework
Documentation
use crate::errors::*;
use crate::upstream;
use http::Method;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use url::Url;

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Sessions {}

impl Sessions {
    pub async fn create_oci_auth_session(&mut self, auth: &OciAuth) -> Result<Option<String>> {
        let auth_resp = upstream::send_req(Method::GET, auth.url.clone(), None, false).await?;
        if let Some(www_auth) = auth_resp.headers().get("Www-Authenticate") {
            let www_auth = www_auth
                .to_str()
                .context("Www-Authenticate header contains invalid utf-8")?;
            let www_auth = www_auth
                .parse::<WwwAuthenticate>()
                .context("Failed to parse Www-Authenticate header")?;
            trace!("Got Www-Authenticate header: {:?}", www_auth);

            let realm = www_auth
                .realm
                .context("Missing realm in Www-Authenticate header")?;
            let service = www_auth
                .service
                .context("Missing service in Www-Authenticate header")?;
            let mut realm_url = realm
                .parse::<Url>()
                .context("Failed to parse realm into url")?;

            if !auth.scopes.is_empty() {
                let scopes = auth.scopes.join(" ");
                realm_url.query_pairs_mut().append_pair("scope", &scopes);
            }
            realm_url.query_pairs_mut().append_pair("service", &service);

            let resp = upstream::send_req(Method::GET, realm_url, None, false)
                .await?
                .error_for_status()?
                .json::<TokenResponse>()
                .await?;

            Ok(Some(resp.token))
        } else {
            Ok(None)
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OciAuth {
    pub url: Url,
    #[serde(default)]
    pub scopes: Vec<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WwwAuthenticate {
    realm: Option<String>,
    service: Option<String>,
    scope: Option<String>,
}

impl FromStr for WwwAuthenticate {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let mut s = s
            .strip_prefix("Bearer ")
            .context("Www-Authenticate header is expected to start with `Bearer `")?;
        let mut www = WwwAuthenticate::default();
        while !s.is_empty() {
            let idx = s.find("=\"").context("Failed to find end of key")?;
            let key = &s[..idx];
            let remaining = &s[idx + 2..];

            let idx = remaining
                .find('\"')
                .context("Failed to find end of value")?;
            let value = &remaining[..idx];
            let remaining = &remaining[idx + 1..];

            match key {
                "realm" => www.realm = Some(value.to_string()),
                "service" => www.service = Some(value.to_string()),
                "scope" => www.scope = Some(value.to_string()),
                _ => debug!("Skipping unknown Www-Authenticate header: {:?}", key),
            }

            s = remaining.strip_prefix(',').unwrap_or(remaining);
        }
        Ok(www)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenResponse {
    token: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_www_authenticate() -> Result<()> {
        let header = "Bearer realm=\"https://ghcr.io/token\",service=\"ghcr.io\",scope=\"repository:user/image:pull\"";
        let parsed = header.parse::<WwwAuthenticate>()?;
        assert_eq!(
            parsed,
            WwwAuthenticate {
                realm: Some("https://ghcr.io/token".to_string()),
                service: Some("ghcr.io".to_string()),
                scope: Some("repository:user/image:pull".to_string()),
            }
        );
        Ok(())
    }
}