-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(oci): init oci api key sign support (#407)
- Loading branch information
Showing
7 changed files
with
243 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)), | ||
})) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |