forked from mirrors/kingfisher
- Fixed issue when more than 1 named capture group is used in a rule variable
- Added 2 new liquid template filters: 'b64dec' and 'es256_sign' - Added custom validator for Coinbase, and a Coinbase rule that uses it
This commit is contained in:
parent
aaabcbd499
commit
e73aec9d70
11 changed files with 369 additions and 22 deletions
113
src/validation/coinbase.rs
Normal file
113
src/validation/coinbase.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use chrono::Utc;
|
||||
use p256::{
|
||||
ecdsa::{signature::Signer, SigningKey},
|
||||
pkcs8::DecodePrivateKey,
|
||||
SecretKey,
|
||||
};
|
||||
use rand::TryRngCore;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
use crate::validation::{httpvalidation, Cache, CachedResponse, VALIDATION_CACHE_SECONDS};
|
||||
|
||||
pub fn generate_coinbase_cache_key(cred_name: &str, private_key: &str) -> String {
|
||||
let mut h = Sha1::new();
|
||||
h.update(cred_name.as_bytes());
|
||||
h.update(b"\0");
|
||||
h.update(private_key.as_bytes());
|
||||
format!("COINBASE:{:x}", h.finalize())
|
||||
}
|
||||
|
||||
pub async fn validate_cdp_api_key(
|
||||
cred_name: &str,
|
||||
private_key_pem: &str,
|
||||
client: &Client,
|
||||
parser: &liquid::Parser,
|
||||
cache: &Cache,
|
||||
) -> Result<(bool, String)> {
|
||||
let cache_key = generate_coinbase_cache_key(cred_name, private_key_pem);
|
||||
if let Some(entry) = cache.get(&cache_key) {
|
||||
let c = entry.value();
|
||||
if c.timestamp.elapsed() < Duration::from_secs(VALIDATION_CACHE_SECONDS) {
|
||||
return Ok((c.is_valid, c.body.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let jwt = build_jwt("GET", "api.coinbase.com", "/v2/user", cred_name, private_key_pem)?;
|
||||
|
||||
let url = Url::parse("https://api.coinbase.com/v2/user")?;
|
||||
let headers = BTreeMap::from([("Authorization".to_string(), format!("Bearer {}", jwt))]);
|
||||
let rb = httpvalidation::build_request_builder(
|
||||
client,
|
||||
"GET",
|
||||
&url,
|
||||
&headers,
|
||||
&None,
|
||||
parser,
|
||||
&liquid::Object::new(),
|
||||
)
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
let resp =
|
||||
httpvalidation::retry_request(rb, 1, Duration::from_millis(500), Duration::from_secs(2))
|
||||
.await?;
|
||||
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
let ok = status == StatusCode::OK;
|
||||
let msg = format!("{body}");
|
||||
|
||||
cache.insert(cache_key.clone(), CachedResponse::new(msg.clone(), status, ok));
|
||||
|
||||
Ok((ok, msg))
|
||||
}
|
||||
|
||||
fn build_jwt(
|
||||
method: &str,
|
||||
host: &str,
|
||||
endpoint: &str,
|
||||
cred_name: &str,
|
||||
pem: &str,
|
||||
) -> Result<String> {
|
||||
let pem =
|
||||
pem.replace("\r\n", "\n").replace("\\r\\n", "\n").replace("\\n", "\n").replace("\r", "\n");
|
||||
let secret_key = SecretKey::from_sec1_pem(&pem)
|
||||
.or_else(|_| SecretKey::from_pkcs8_pem(&pem))
|
||||
.map_err(|e| anyhow!("invalid EC key: {e}"))?;
|
||||
let signing_key = SigningKey::from(secret_key);
|
||||
|
||||
let mut rng = OsRng;
|
||||
let mut nonce = [0u8; 16];
|
||||
|
||||
let _ = rng.try_fill_bytes(&mut nonce);
|
||||
|
||||
let header = serde_json::json!({
|
||||
"typ": "JWT",
|
||||
"alg": "ES256",
|
||||
"kid": cred_name,
|
||||
"nonce": hex::encode(nonce),
|
||||
});
|
||||
let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string());
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
let claims = serde_json::json!({
|
||||
"sub": cred_name,
|
||||
"iss": "cdp",
|
||||
"nbf": now,
|
||||
"exp": now + 60,
|
||||
"uri": format!("{} {}{}", method, host, endpoint),
|
||||
});
|
||||
let claims_b64 = URL_SAFE_NO_PAD.encode(claims.to_string());
|
||||
|
||||
let signing_input = format!("{header_b64}.{claims_b64}");
|
||||
let sig: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
|
||||
let sig_b64 = URL_SAFE_NO_PAD.encode(sig.to_bytes());
|
||||
|
||||
Ok(format!("{signing_input}.{sig_b64}"))
|
||||
}
|
||||
|
|
@ -2,19 +2,47 @@ use reqwest::Url;
|
|||
use tokio::net::lookup_host;
|
||||
|
||||
use crate::validation::SerializableCaptures;
|
||||
pub fn process_captures(captures: &SerializableCaptures) -> Vec<(String, String, usize, usize)> {
|
||||
let has_multiple_captures = captures.captures.len() > 1;
|
||||
|
||||
// pub fn process_captures(captures: &SerializableCaptures) -> Vec<(String, String, usize, usize)> {
|
||||
// let has_multiple_captures = captures.captures.len() > 1;
|
||||
// captures
|
||||
// .captures
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .filter(|(idx, _)| !has_multiple_captures || *idx > 0)
|
||||
// .map(|(_, capture)| {
|
||||
// let name = capture.name.as_ref().map_or("TOKEN".to_string(), |n| n.to_uppercase());
|
||||
// (name, capture.value.clone().into_owned(), capture.start, capture.end)
|
||||
// })
|
||||
// .collect()
|
||||
// }
|
||||
|
||||
/// Return (NAME, value, start, end) for every capture we care about.
|
||||
///
|
||||
/// * If a capture has a name, use that (upper-cased)
|
||||
/// * If it’s unnamed, fall back to `"TOKEN"`
|
||||
/// * Skip the unnamed “whole-match” capture **only when** there are
|
||||
/// additional captures to return.
|
||||
pub fn process_captures(
|
||||
captures: &SerializableCaptures,
|
||||
) -> Vec<(String, String, usize, usize)> {
|
||||
let multiple = captures.captures.len() > 1;
|
||||
|
||||
captures
|
||||
.captures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(idx, _)| !has_multiple_captures || *idx > 0)
|
||||
.map(|(_, capture)| {
|
||||
let name = capture.name.as_ref().map_or("TOKEN".to_string(), |n| n.to_uppercase());
|
||||
(name, capture.value.clone().into_owned(), capture.start, capture.end)
|
||||
.filter(|cap| multiple.then(|| cap.name.is_some()).unwrap_or(true))
|
||||
.map(|cap| {
|
||||
let name = cap
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|n| n.to_uppercase())
|
||||
.unwrap_or_else(|| "TOKEN".to_string());
|
||||
(name, cap.value.clone().into_owned(), cap.start, cap.end)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_closest_variable(
|
||||
captures: &[(String, String, usize, usize)],
|
||||
target_value: &String,
|
||||
|
|
@ -47,6 +75,7 @@ pub fn find_closest_variable(
|
|||
}
|
||||
closest_value
|
||||
}
|
||||
|
||||
pub async fn check_url_resolvable(url: &Url) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let host = url.host_str().ok_or("No host in URL")?;
|
||||
let port = url.port().unwrap_or(if url.scheme() == "https" { 443 } else { 80 });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue