Skip to content

Commit

Permalink
feat(oci): init oci api key sign support (#407)
Browse files Browse the repository at this point in the history
  • Loading branch information
zwpaper authored Feb 3, 2024
1 parent 5ca9b2f commit dfd0bd6
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 1 deletion.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ services-all = [
"services-azblob",
"services-google",
"services-huaweicloud",
"services-oracle",
"services-tencent",
]

Expand All @@ -48,6 +49,7 @@ services-google = [
"dep:jsonwebtoken",
]
services-huaweicloud = ["dep:serde", "dep:serde_json", "dep:once_cell"]
services-oracle = ["dep:reqwest", "dep:rsa", "dep:toml"]
services-tencent = ["dep:reqwest", "dep:serde", "dep:serde_json"]

[[bench]]
Expand All @@ -70,12 +72,13 @@ percent-encoding = "2"
quick-xml = { version = "0.31", features = ["serialize"], optional = true }
rand = "0.8.5"
reqwest = { version = "0.11", default-features = false, optional = true }
rsa = { version = "0.9.2" }
rsa = { version = "0.9.2", features = ["pkcs5", "sha2"], optional = true }
rust-ini = { version = "0.20", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
sha1 = "0.10"
sha2 = { version = "0.10", features = ["oid"] }
toml = { version = "0.8.9", optional = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
home = "0.5"
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ mod huaweicloud;
#[cfg(feature = "services-huaweicloud")]
pub use huaweicloud::*;

#[cfg(feature = "services-oracle")]
mod oracle;
#[cfg(feature = "services-oracle")]
pub use oracle::*;

#[cfg(feature = "services-tencent")]
mod tencent;
#[cfg(feature = "services-tencent")]
Expand Down
31 changes: 31 additions & 0 deletions src/oracle/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use anyhow::Result;
use serde::Deserialize;
use std::fs::read_to_string;
use toml::from_str;

/// Config carries all the configuration for Oracle services.
/// will be loaded from default config file ~/.oci/config
#[derive(Clone, Default, Deserialize)]
#[cfg_attr(test, derive(Debug))]
pub struct Config {
/// userID for Oracle Cloud Infrastructure.
pub user: String,
/// tenancyID for Oracle Cloud Infrastructure.
pub tenancy: String,
/// region for Oracle Cloud Infrastructure.
pub region: String,
/// private key file for Oracle Cloud Infrastructure.
pub key_file: Option<String>,
/// fingerprint for the key_file.
pub fingerprint: Option<String>,
}

impl Config {
/// Load config from env.
pub fn from_config(path: &str) -> Result<Self> {
let content = read_to_string(path)?;
let config = from_str(&content)?;

Ok(config)
}
}
2 changes: 2 additions & 0 deletions src/oracle/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Env values used in oracle cloud infrastructure services.
pub const ORACLE_CONFIG_PATH: &str = "~/.oci/config";
90 changes: 90 additions & 0 deletions src/oracle/credential.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::sync::Arc;
use std::sync::Mutex;

use anyhow::Result;
use log::debug;

use super::config::Config;
use super::constants::ORACLE_CONFIG_PATH;
use crate::time::now;
use crate::time::DateTime;

/// Credential that holds the API private key.
/// private_key_path is optional, because some other credential will be added later
#[derive(Default, Clone)]
#[cfg_attr(test, derive(Debug))]
pub struct Credential {
/// TenantID for Oracle Cloud Infrastructure.
pub tenancy: String,
/// UserID for Oracle Cloud Infrastructure.
pub user: String,
/// API Private Key for credential.
pub key_file: Option<String>,
/// Fingerprint of the API Key.
pub fingerprint: Option<String>,
/// expires in for credential.
pub expires_in: Option<DateTime>,
}

impl Credential {
/// is current cred is valid?
pub fn is_valid(&self) -> bool {
self.key_file.is_some()
&& self.fingerprint.is_some()
&& self.expires_in.unwrap_or_default() > now()
}
}

/// Loader will load credential from different methods.
#[derive(Default)]
#[cfg_attr(test, derive(Debug))]
pub struct Loader {
credential: Arc<Mutex<Option<Credential>>>,
}

impl Loader {
/// Load credential.
pub async fn load(&self) -> Result<Option<Credential>> {
// Return cached credential if it's valid.
match self.credential.lock().expect("lock poisoned").clone() {
Some(cred) if cred.is_valid() => return Ok(Some(cred)),
_ => (),
}

let cred = if let Some(cred) = self.load_inner().await? {
cred
} else {
return Ok(None);
};

let mut lock = self.credential.lock().expect("lock poisoned");
*lock = Some(cred.clone());

Ok(Some(cred))
}

async fn load_inner(&self) -> Result<Option<Credential>> {
if let Ok(Some(cred)) = self
.load_via_config()
.map_err(|err| debug!("load credential via static failed: {err:?}"))
{
return Ok(Some(cred));
}

Ok(None)
}

fn load_via_config(&self) -> Result<Option<Credential>> {
let config = Config::from_config(ORACLE_CONFIG_PATH)?;

Ok(Some(Credential {
tenancy: config.tenancy,
user: config.user,
key_file: config.key_file,
fingerprint: config.fingerprint,
// Set expires_in to 10 minutes to enforce re-read
// from file.
expires_in: Some(now() + chrono::Duration::minutes(10)),
}))
}
}
14 changes: 14 additions & 0 deletions src/oracle/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//! Oracle Cloud Infrastructure service signer
//!

mod oci;
pub use oci::APIKeySigner as OCIAPIKeySigner;

mod config;
pub use config::Config as OCIConfig;

mod credential;
pub use credential::Credential as OCICredential;
pub use credential::Loader as OCILoader;

mod constants;
97 changes: 97 additions & 0 deletions src/oracle/oci.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! Oracle Cloud Infrastructure Singer

use anyhow::{Error, Result};
use base64::{engine::general_purpose, Engine as _};
use http::{
header::{AUTHORIZATION, DATE},
HeaderValue,
};
use log::debug;
use rsa::pkcs1v15::SigningKey;
use rsa::sha2::Sha256;
use rsa::signature::{SignatureEncoding, Signer};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use std::fmt::Write;

use super::credential::Credential;
use crate::ctx::SigningContext;
use crate::request::SignableRequest;
use crate::time;
use crate::time::DateTime;

/// Singer for Oracle Cloud Infrastructure using API Key.
#[derive(Default)]
pub struct APIKeySigner {}

impl APIKeySigner {
/// Building a signing context.
fn build(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<SigningContext> {
let now = time::now();
let mut ctx = req.build()?;

let string_to_sign = string_to_sign(&mut ctx, now)?;
let private_key = if let Some(path) = &cred.key_file {
RsaPrivateKey::read_pkcs8_pem_file(path)?
} else {
return Err(Error::msg("no private key"));
};
let signing_key = SigningKey::<Sha256>::new(private_key);
let signature = signing_key.try_sign(string_to_sign.as_bytes())?;
let encoded_signature = general_purpose::STANDARD.encode(signature.to_bytes());

ctx.headers
.insert(DATE, HeaderValue::from_str(&time::format_http_date(now))?);
if let Some(fp) = &cred.fingerprint {
let mut auth_value = String::new();
write!(auth_value, "Signature version=\"1\",")?;
write!(auth_value, "headers=\"date (request-target) host\",")?;
write!(
auth_value,
"keyId=\"{}/{}/{}\",",
cred.tenancy, cred.user, &fp
)?;
write!(auth_value, "algorithm=\"rsa-sha256\",")?;
write!(auth_value, "signature=\"{}\"", encoded_signature)?;
ctx.headers
.insert(AUTHORIZATION, HeaderValue::from_str(&auth_value)?);
} else {
return Err(Error::msg("no fingerprint"));
}

Ok(ctx)
}

/// Signing request with header.
pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> {
let ctx = self.build(req, cred)?;

req.apply(ctx)
}
}

/// Construct string to sign.
///
/// # Format
///
/// ```text
/// "date: {Date}" + "\n"
/// + "(request-target): {verb} {uri}" + "\n"
/// + "host: {Host}"
/// ```
fn string_to_sign(ctx: &mut SigningContext, now: DateTime) -> Result<String> {
let string_to_sign = {
let mut f = String::new();
writeln!(f, "date: {}", time::format_http_date(now))?;
writeln!(
f,
"(request-target): {} {}",
ctx.method.as_str().to_lowercase(),
ctx.path
)?;
write!(f, "host: {}", ctx.authority)?;
f
};

debug!("string to sign: {}", &string_to_sign);
Ok(string_to_sign)
}

0 comments on commit dfd0bd6

Please sign in to comment.