use crate::error::{Error, Result};
use base64::{Engine as _, engine::general_purpose};
use rsa::RsaPrivateKey;
use rsa::pkcs1v15::SigningKey;
use rsa::pkcs8::DecodePrivateKey;
use rsa::signature::{SignatureEncoding, Signer as RsaSigner};
use sha2::Sha256;
use std::fs;
use std::sync::Arc;
use tempfile::NamedTempFile;
pub struct OciSigner {
user_id: String,
tenancy_id: String,
fingerprint: String,
private_key: Arc<RsaPrivateKey>,
_temp_key_file: Option<NamedTempFile>, }
impl Clone for OciSigner {
fn clone(&self) -> Self {
Self {
user_id: self.user_id.clone(),
tenancy_id: self.tenancy_id.clone(),
fingerprint: self.fingerprint.clone(),
private_key: Arc::clone(&self.private_key),
_temp_key_file: None,
}
}
}
impl OciSigner {
pub fn new(
user_id: &str,
tenancy_id: &str,
fingerprint: &str,
private_key: &str,
) -> Result<Self> {
let is_pem_content =
private_key.contains("-----BEGIN") && private_key.contains("-----END");
let (private_key_obj, temp_file) = if is_pem_content {
let temp_file = NamedTempFile::new()
.map_err(|e| Error::Other(format!("Failed to create temp file: {e}")))?;
fs::write(temp_file.path(), private_key.as_bytes()).map_err(|e| {
Error::Other(format!("Failed to write private key to temp file: {e}"))
})?;
let key = RsaPrivateKey::read_pkcs8_pem_file(temp_file.path()).map_err(|e| {
Error::ConfigError(format!("Failed to parse private key: {e}"))
})?;
(key, Some(temp_file))
} else {
let key = RsaPrivateKey::read_pkcs8_pem_file(private_key).map_err(|e| {
Error::ConfigError(format!("Failed to read private key from file: {e}"))
})?;
(key, None)
};
Ok(Self {
user_id: user_id.to_string(),
tenancy_id: tenancy_id.to_string(),
fingerprint: fingerprint.to_string(),
private_key: Arc::new(private_key_obj),
_temp_key_file: temp_file,
})
}
pub fn user_id(&self) -> &str {
&self.user_id
}
pub fn tenancy_id(&self) -> &str {
&self.tenancy_id
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn sign_request(
&self,
method: &str,
path: &str,
host: &str,
body: Option<&str>,
) -> Result<(String, String)> {
self.sign_request_full(method, path, host, body, None)
}
pub fn sign_request_with_content_type(
&self,
method: &str,
path: &str,
host: &str,
body: Option<&str>,
content_type: &str,
) -> Result<(String, String)> {
self.sign_request_full(method, path, host, body, Some(content_type))
}
fn sign_request_full(
&self,
method: &str,
path: &str,
host: &str,
body: Option<&str>,
content_type: Option<&str>,
) -> Result<(String, String)> {
let date = httpdate::fmt_http_date(std::time::SystemTime::now());
self.sign_request_with_date_and_content_type(method, path, host, body, &date, content_type)
}
pub fn sign_request_with_date_and_content_type(
&self,
method: &str,
path: &str,
host: &str,
body: Option<&str>,
date: &str,
content_type: Option<&str>,
) -> Result<(String, String)> {
let signing_string = if let Some(body_content) = body {
let body_sha256 = {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(body_content.as_bytes());
let result = hasher.finalize();
general_purpose::STANDARD.encode(result)
};
let content_length = body_content.len().to_string();
let content_type_value = content_type.unwrap_or("application/json");
format!(
"date: {}\n(request-target): {} {}\nhost: {}\ncontent-length: {}\ncontent-type: {}\nx-content-sha256: {}",
date,
method.to_lowercase(),
path,
host,
content_length,
content_type_value,
body_sha256
)
} else {
format!(
"date: {}\n(request-target): {} {}\nhost: {}",
date,
method.to_lowercase(),
path,
host
)
};
let signing_key = SigningKey::<Sha256>::new((*self.private_key).clone());
let signature = signing_key
.try_sign(signing_string.as_bytes())
.map_err(|e| Error::AuthError(format!("Failed to sign request: {e}")))?;
let encoded_signature = general_purpose::STANDARD.encode(signature.to_bytes());
let headers_list = if body.is_some() {
"date (request-target) host content-length content-type x-content-sha256"
} else {
"date (request-target) host"
};
let key_id = format!("{}/{}/{}", self.tenancy_id, self.user_id, self.fingerprint);
let authorization = format!(
"Signature version=\"1\",headers=\"{headers_list}\",keyId=\"{key_id}\",algorithm=\"rsa-sha256\",signature=\"{encoded_signature}\""
);
Ok((date.to_string(), authorization))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signer_creation_with_pem_content() {
let pem_content = r#"-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1+fWIcPm15j8aB2v3e1pDzLdOHLJaSecrNjAP1LfTkRcJL
iEWXiZLp6dPT3gJw/WmF9v6K8N8rFvQbSb3VvTlqcJYY/0KPJ7Pqe3gJ/tHkI1HN
6bvnm5X3O4TLNWBxOW1PQ2SdRqBJYT0x0rRqVYMiB0g1RiPcCtf1fI7RsYlGtPH8
oF0r7fLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL
-----END PRIVATE KEY-----"#;
let result = OciSigner::new(
"ocid1.user.oc1..test",
"ocid1.tenancy.oc1..test",
"aa:bb:cc:dd:ee:ff",
pem_content,
);
assert!(result.is_err()); }
#[test]
fn test_signing_string_format_without_body() {
}
}