Add signature tests, update dependencies, remove Cargo.lock from git
This commit is contained in:
parent
6b4f798f76
commit
c56f526914
6 changed files with 189 additions and 2306 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
/target
|
/target
|
||||||
/.idea
|
/.idea
|
||||||
|
/Cargo.lock
|
||||||
|
|
2263
Cargo.lock
generated
2263
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
52
Cargo.toml
52
Cargo.toml
|
@ -9,41 +9,41 @@ repository = "https://github.com/LemmyNet/activitypub-federation-rust"
|
||||||
documentation = "https://docs.rs/activitypub_federation/"
|
documentation = "https://docs.rs/activitypub_federation/"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4.23", features = ["clock"], default-features = false }
|
chrono = { version = "0.4.24", features = ["clock"], default-features = false }
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.159", features = ["derive"] }
|
||||||
async-trait = "0.1.58"
|
async-trait = "0.1.68"
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
serde_json = { version = "1.0.87", features = ["preserve_order"] }
|
serde_json = { version = "1.0.95", features = ["preserve_order"] }
|
||||||
anyhow = "1.0.66"
|
anyhow = "1.0.70"
|
||||||
reqwest = { version = "0.11.12", features = ["json", "stream"] }
|
reqwest = { version = "0.11.16", features = ["json", "stream"] }
|
||||||
reqwest-middleware = "0.2.0"
|
reqwest-middleware = "0.2.1"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
base64 = "0.13.1"
|
base64 = "0.21.0"
|
||||||
openssl = "0.10.42"
|
openssl = "0.10.48"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.17.1"
|
||||||
http = "0.2.8"
|
http = "0.2.9"
|
||||||
sha2 = "0.10.6"
|
sha2 = "0.10.6"
|
||||||
background-jobs = "0.13.0"
|
background-jobs = "0.13.0"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.40"
|
||||||
derive_builder = "0.12.0"
|
derive_builder = "0.12.0"
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
dyn-clone = "1.0.9"
|
dyn-clone = "1.0.11"
|
||||||
enum_delegate = "0.2.0"
|
enum_delegate = "0.2.0"
|
||||||
httpdate = "1.0.2"
|
httpdate = "1.0.2"
|
||||||
http-signature-normalization-reqwest = { version = "0.7.1", default-features = false, features = ["sha-2", "middleware"] }
|
http-signature-normalization-reqwest = { version = "0.8.0", default-features = false, features = ["sha-2", "middleware"] }
|
||||||
http-signature-normalization = "0.6.0"
|
http-signature-normalization = "0.7.0"
|
||||||
actix-rt = "2.7.0"
|
actix-rt = "2.8.0"
|
||||||
bytes = "1.3.0"
|
bytes = "1.4.0"
|
||||||
futures-core = { version = "0.3.25", default-features = false }
|
futures-core = { version = "0.3.27", default-features = false }
|
||||||
pin-project-lite = "0.2.9"
|
pin-project-lite = "0.2.9"
|
||||||
activitystreams-kinds = "0.2.1"
|
activitystreams-kinds = "0.3.0"
|
||||||
regex = { version = "1.7.1", default-features = false, features = ["std"] }
|
regex = { version = "1.7.3", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
# Actix-web
|
# Actix-web
|
||||||
actix-web = { version = "4.2.1", default-features = false, optional = true }
|
actix-web = { version = "4.3.1", default-features = false, optional = true }
|
||||||
|
|
||||||
# Axum
|
# Axum
|
||||||
axum = { version = "0.6.0", features = ["json", "headers"], default-features = false, optional = true }
|
axum = { version = "0.6.12", features = ["json", "headers"], default-features = false, optional = true }
|
||||||
tower = { version = "0.4.13", optional = true }
|
tower = { version = "0.4.13", optional = true }
|
||||||
hyper = { version = "0.14", optional = true }
|
hyper = { version = "0.14", optional = true }
|
||||||
displaydoc = "0.2.3"
|
displaydoc = "0.2.3"
|
||||||
|
@ -55,10 +55,10 @@ axum = ["dep:axum", "dep:tower", "dep:hyper"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
env_logger = "0.9.3"
|
env_logger = "0.10.0"
|
||||||
tower-http = { version = "0.3", features = ["map-request-body", "util"] }
|
tower-http = { version = "0.4.0", features = ["map-request-body", "util"] }
|
||||||
axum = { version = "0.6.0", features = ["http1", "tokio", "query"], default-features = false }
|
axum = { version = "0.6.12", features = ["http1", "tokio", "query"], default-features = false }
|
||||||
axum-macros = "0.3.4"
|
axum-macros = "0.3.7"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
strip = "symbols"
|
strip = "symbols"
|
||||||
|
|
|
@ -194,7 +194,7 @@ async fn do_send(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
|
pub(crate) fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
|
||||||
let mut host = inbox_url.domain().expect("read inbox domain").to_string();
|
let mut host = inbox_url.domain().expect("read inbox domain").to_string();
|
||||||
if let Some(port) = inbox_url.port() {
|
if let Some(port) = inbox_url.port() {
|
||||||
host = format!("{}:{}", host, port);
|
host = format!("{}:{}", host, port);
|
||||||
|
|
|
@ -55,6 +55,7 @@ where
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
activity_queue::generate_request_headers,
|
||||||
config::FederationConfig,
|
config::FederationConfig,
|
||||||
http_signatures::sign_request,
|
http_signatures::sign_request,
|
||||||
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
|
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
|
||||||
|
@ -62,6 +63,7 @@ mod test {
|
||||||
use actix_web::test::TestRequest;
|
use actix_web::test::TestRequest;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_receive_activity() {
|
async fn test_receive_activity() {
|
||||||
|
@ -109,8 +111,11 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn setup_receive_test() -> (String, TestRequest, FederationConfig<DbConnection>) {
|
async fn setup_receive_test() -> (String, TestRequest, FederationConfig<DbConnection>) {
|
||||||
let request_builder =
|
let inbox = "https://example.com/inbox";
|
||||||
ClientWithMiddleware::from(Client::default()).post("https://example.com/inbox");
|
let headers = generate_request_headers(&Url::parse(inbox).unwrap());
|
||||||
|
let request_builder = ClientWithMiddleware::from(Client::default())
|
||||||
|
.post(inbox)
|
||||||
|
.headers(headers);
|
||||||
let activity = Follow {
|
let activity = Follow {
|
||||||
actor: ObjectId::parse("http://localhost:123").unwrap(),
|
actor: ObjectId::parse("http://localhost:123").unwrap(),
|
||||||
object: ObjectId::parse("http://localhost:124").unwrap(),
|
object: ObjectId::parse("http://localhost:124").unwrap(),
|
||||||
|
|
|
@ -9,9 +9,10 @@ use crate::{
|
||||||
error::{Error, Error::ActivitySignatureInvalid},
|
error::{Error, Error::ActivitySignatureInvalid},
|
||||||
protocol::public_key::main_key_id,
|
protocol::public_key::main_key_id,
|
||||||
};
|
};
|
||||||
|
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
|
||||||
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
|
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
|
||||||
use http_signature_normalization_reqwest::prelude::{Config, SignExt};
|
use http_signature_normalization_reqwest::prelude::{Config, SignExt};
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::Lazy;
|
||||||
use openssl::{
|
use openssl::{
|
||||||
hash::MessageDigest,
|
hash::MessageDigest,
|
||||||
pkey::PKey,
|
pkey::PKey,
|
||||||
|
@ -25,8 +26,6 @@ use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
static HTTP_SIG_CONFIG: OnceCell<Config> = OnceCell::new();
|
|
||||||
|
|
||||||
/// A private/public key pair used for HTTP signatures
|
/// A private/public key pair used for HTTP signatures
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Keypair {
|
pub struct Keypair {
|
||||||
|
@ -64,15 +63,14 @@ pub(crate) async fn sign_request(
|
||||||
private_key: String,
|
private_key: String,
|
||||||
http_signature_compat: bool,
|
http_signature_compat: bool,
|
||||||
) -> Result<Request, anyhow::Error> {
|
) -> Result<Request, anyhow::Error> {
|
||||||
|
static CONFIG: Lazy<Config> = Lazy::new(Config::new);
|
||||||
|
static CONFIG_COMPAT: Lazy<Config> = Lazy::new(|| Config::new().mastodon_compat());
|
||||||
|
|
||||||
let key_id = main_key_id(&actor_id);
|
let key_id = main_key_id(&actor_id);
|
||||||
let sig_conf = HTTP_SIG_CONFIG.get_or_init(|| {
|
let sig_conf = match http_signature_compat {
|
||||||
let c = Config::new();
|
false => CONFIG.clone(),
|
||||||
if http_signature_compat {
|
true => CONFIG_COMPAT.clone(),
|
||||||
c.mastodon_compat()
|
};
|
||||||
} else {
|
|
||||||
c
|
|
||||||
}
|
|
||||||
});
|
|
||||||
request_builder
|
request_builder
|
||||||
.signature_with_digest(
|
.signature_with_digest(
|
||||||
sig_conf.clone(),
|
sig_conf.clone(),
|
||||||
|
@ -84,7 +82,7 @@ pub(crate) async fn sign_request(
|
||||||
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
|
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
|
||||||
signer.update(signing_string.as_bytes())?;
|
signer.update(signing_string.as_bytes())?;
|
||||||
|
|
||||||
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, anyhow::Error>
|
Ok(Base64.encode(signer.sign_to_vec()?)) as Result<_, anyhow::Error>
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -122,7 +120,7 @@ where
|
||||||
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
|
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
|
||||||
verifier.update(signing_string.as_bytes())?;
|
verifier.update(signing_string.as_bytes())?;
|
||||||
Ok(verifier.verify(&base64::decode(signature)?)?)
|
Ok(verifier.verify(&Base64.decode(signature)?)?)
|
||||||
})
|
})
|
||||||
.map_err(Error::other)?;
|
.map_err(Error::other)?;
|
||||||
|
|
||||||
|
@ -179,10 +177,152 @@ pub(crate) fn verify_inbox_hash(
|
||||||
|
|
||||||
for part in digest {
|
for part in digest {
|
||||||
hasher.update(body);
|
hasher.update(body);
|
||||||
if base64::encode(hasher.finalize_reset()) != part.digest {
|
if Base64.encode(hasher.finalize_reset()) != part.digest {
|
||||||
return Err(Error::ActivityBodyDigestInvalid);
|
return Err(Error::ActivityBodyDigestInvalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::activity_queue::generate_request_headers;
|
||||||
|
use reqwest::Client;
|
||||||
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
|
||||||
|
static INBOX_URL: Lazy<Url> =
|
||||||
|
Lazy::new(|| Url::parse("https://example.com/u/alice/inbox").unwrap());
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_sign() {
|
||||||
|
let mut headers = generate_request_headers(&INBOX_URL);
|
||||||
|
// use hardcoded date in order to test against hardcoded signature
|
||||||
|
headers.insert(
|
||||||
|
"date",
|
||||||
|
HeaderValue::from_str("Tue, 28 Mar 2023 21:03:44 GMT").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let request_builder = ClientWithMiddleware::from(Client::new())
|
||||||
|
.post(INBOX_URL.to_string())
|
||||||
|
.headers(headers);
|
||||||
|
let request = sign_request(
|
||||||
|
request_builder,
|
||||||
|
ACTOR_ID.clone(),
|
||||||
|
"my activity".to_string(),
|
||||||
|
test_keypair().private_key,
|
||||||
|
// set this to prevent created/expires headers to be generated and inserted
|
||||||
|
// automatically from current time
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let signature = request
|
||||||
|
.headers()
|
||||||
|
.get("signature")
|
||||||
|
.unwrap()
|
||||||
|
.to_str()
|
||||||
|
.unwrap();
|
||||||
|
let expected_signature = concat!(
|
||||||
|
"keyId=\"https://example.com/u/alice#main-key\",",
|
||||||
|
"algorithm=\"hs2019\",",
|
||||||
|
"headers=\"(request-target) content-type date digest host\",",
|
||||||
|
"signature=\"BpZhHNqzd6d6jhWOxyJ0jXwWWxiKMNK7i3mrr/5mVFnH7fUpicwqw8cSYVr",
|
||||||
|
"cwWjt0I07HW7rZFUfIdSgCoOEdvxtrccF/hTrwYgm8O6SQRHl1UfFtDR6e9EpfPieVmTjo0",
|
||||||
|
"QVfyzLLa41rmnz/yFqqer/v0kcdED51/dGe8NCGPBbhgK6C4oh7r+XHsQZMIhh38BcfZVWN",
|
||||||
|
"YaMqgyhFxu2f34IKnOEk6NjSaNtO+PzQUhbksTvH0Vvi6R0dtQINJFdONVBl4AwDC1INeF5",
|
||||||
|
"uhQo/SaKHfP3UitUHdM5Pbn+LhZYDB9AaQAW5ZGD43Aw15ecwsnKi4HcjV8nBw4zehlvaQ==\""
|
||||||
|
);
|
||||||
|
assert_eq!(signature, expected_signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_verify() {
|
||||||
|
let headers = generate_request_headers(&INBOX_URL);
|
||||||
|
let request_builder = ClientWithMiddleware::from(Client::new())
|
||||||
|
.post(INBOX_URL.to_string())
|
||||||
|
.headers(headers);
|
||||||
|
let request = sign_request(
|
||||||
|
request_builder,
|
||||||
|
ACTOR_ID.clone(),
|
||||||
|
"my activity".to_string(),
|
||||||
|
test_keypair().private_key,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let valid = verify_signature(
|
||||||
|
request.headers(),
|
||||||
|
request.method(),
|
||||||
|
&Uri::from_str(request.url().as_str()).unwrap(),
|
||||||
|
&test_keypair().public_key,
|
||||||
|
);
|
||||||
|
println!("{:?}", &valid);
|
||||||
|
assert!(valid.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verify_inbox_hash_valid() {
|
||||||
|
let digest_header =
|
||||||
|
HeaderValue::from_static("SHA-256=lzFT+G7C2hdI5j8M+FuJg1tC+O6AGMVJhooTCKGfbKM=");
|
||||||
|
let body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
|
||||||
|
let valid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
|
||||||
|
println!("{:?}", &valid);
|
||||||
|
assert!(valid.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verify_inbox_hash_not_valid() {
|
||||||
|
let digest_header =
|
||||||
|
HeaderValue::from_static("SHA-256=Z9h7DJfYWjffXw2XftmWCnpEaK/yqOHKvzCIzIaqgbU=");
|
||||||
|
let body = "lorem ipsum";
|
||||||
|
let invalid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
|
||||||
|
assert_eq!(invalid, Err(Error::ActivityBodyDigestInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_keypair() -> Keypair {
|
||||||
|
let rsa = Rsa::private_key_from_pem(PRIVATE_KEY.as_bytes()).unwrap();
|
||||||
|
let pkey = PKey::from_rsa(rsa).unwrap();
|
||||||
|
let private_key = pkey.private_key_to_pem_pkcs8().unwrap();
|
||||||
|
let public_key = pkey.public_key_to_pem().unwrap();
|
||||||
|
Keypair {
|
||||||
|
private_key: String::from_utf8(private_key).unwrap(),
|
||||||
|
public_key: String::from_utf8(public_key).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hardcoded private key so that signature doesn't change across runs
|
||||||
|
const PRIVATE_KEY: &str = concat!(
|
||||||
|
"-----BEGIN RSA PRIVATE KEY-----\n",
|
||||||
|
"MIIEogIBAAKCAQEA2kZpsvWYrwM9zMQiDwo4k6/VfpK2aDTeVe9ZkcvDrrWfqt72\n",
|
||||||
|
"QSjjtXLa8sxJlEn+/zbnZ1lG3AO/WsKs2jiOycNQHBS1ITnSZKEpdKnAoLUn4k16\n",
|
||||||
|
"YivRmALyLedOfIrvMtQzH8a+kOQ71u2Wa3H9jpkCT5W9OneEBa3VjQp49kcrF3tm\n",
|
||||||
|
"mrEUhfai5GJM4xrdr587y7exkBF4wObepta9opSeuBkPV4QXZPfgmjwW+oOTheVH\n",
|
||||||
|
"6L7yjzvjW92j4/T6XKAcu0kn/aQhR8SiGtPBMyOlcW4S2eDHWf1RlqbNGb5L9Qam\n",
|
||||||
|
"fb0WAymx0ANLUDQyXAu5zViMrd4g8mgdkg7C1wIDAQABAoIBAAHAT0Uvsguz0Frq\n",
|
||||||
|
"0Li8+A4I4U/RQeqW6f9XtHWpl3NSYuqOPJZY2DxypHRB1Iex13x/gBHH/8jwgShR\n",
|
||||||
|
"2x/3ev9kmsLu6f+CcdniCFQdFiRaVh/IFI0Ve7cz5tkcoiuSB2NDNcaYFwIdYqfr\n",
|
||||||
|
"Ytz2OCn2hLQHKB9M9pLMSnDsPmMAOveY11XfhkECrWlh1bx9YPyJScnNKTblB3M+\n",
|
||||||
|
"GhYL3xzuCxPCC9nUfqz7Y8FnZTCmePOwcRflJDTLFs6Bqkv1PZOZWzI+7akaJxfI\n",
|
||||||
|
"SOSw3VkGegsdoGVgHobqT2tqL8vuKM1bs47PFwWjVCGEoOvcC/Ha1+INemWbh7VA\n",
|
||||||
|
"Xa/jvxkCgYEA/+AxeMCLCmH/F696W3RpPdFL25wSYQr1auV2xRfmsT+hhpSp3yz/\n",
|
||||||
|
"ypkazS9TbnSCm18up+jE9rJ1c9VIZrgcTeKzPURzE68RR8uOsa9o9kaUzfyvRAzb\n",
|
||||||
|
"fmQXMvv2rmm9U7srhjpvKo1BcHpQIQYToKt0TOv7soSEY2jGNvaK6i0CgYEA2mGL\n",
|
||||||
|
"sL36WoHF3x2DZNvknLJGjxPSMmdjjfflFRqxKeP+Sf54C4QH/1hxHe/yl/KMBTfa\n",
|
||||||
|
"woBl05SrwTnQ7bOeR8VTmzP53JfkECT5I9h/g8vT8dkz5WQXWNDgy61Imq/UmWwm\n",
|
||||||
|
"DHElGrkF31oy5w6+aZ58Sa5bXhBDYpkUP9+pV5MCgYAW5BCo89i8gg3XKZyxp9Vu\n",
|
||||||
|
"cVXu/KRsSBWyjXq1oTDDNKUXrB8SVy0/C7lpF83H+OZiTf6XiOxuAYMebLtAbUIi\n",
|
||||||
|
"+Z/9YC1HWocaPCy02rNyLNhNIUjwtpHAWeX1arMj4VPNtNXs+TdOwDpVfKvEeI2y\n",
|
||||||
|
"9wO9ifMHgnFxj0MEUcQVtQKBgHg2Mhs8uM+RmEbVjDq9AP9w835XPuIYH6lKyIPx\n",
|
||||||
|
"iYyxwI0i0xojt/NL0BjWuQgDsCg/MuDWpTbvJAzdsrDmqz5+1SMeXXCc/CIW+D5P\n",
|
||||||
|
"MwJt9WGwWuzvSBrQAK6d2NWt7K335on6zp4DM8RbdqHSb+bcIza8D/ebpDxmX8s5\n",
|
||||||
|
"Z5KZAoGAX8u+63w1uy1FLhf48SqmjOqkAjdUZCWEmaim69koAOdTIBSSDOnAqzGu\n",
|
||||||
|
"wIVdLLzI6xTgbYmfErCwpU2v8MfUWr0BDzjQ9G6c5rhcS1BkfxbeAsC42XaVIgCk\n",
|
||||||
|
"2sMNMqi6f96jbp4IQI70BpecsnBAUa+VoT57bZRvy0lW26w9tYI=\n",
|
||||||
|
"-----END RSA PRIVATE KEY-----\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue