Skip to content

Commit 1ddd689

Browse files
committed
Add a module to authenticate by proving private key knowledge
This adds an authentication module to match the service side logic at lightningdevkit/vss-server#79
1 parent 88f3c1f commit 1ddd689

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ categories = ["web-programming::http-client", "cryptography::cryptocurrencies"]
1414
build = "build.rs"
1515

1616
[features]
17-
default = ["lnurl-auth"]
17+
default = ["lnurl-auth", "sigs-auth"]
1818
lnurl-auth = ["dep:bitcoin", "dep:url", "dep:serde", "dep:serde_json", "reqwest/json"]
19+
sigs-auth = ["dep:bitcoin"]
1920

2021
[dependencies]
2122
prost = "0.11.6"

src/headers/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ mod lnurl_auth_jwt;
1212
#[cfg(feature = "lnurl-auth")]
1313
pub use lnurl_auth_jwt::LnurlAuthToJwtProvider;
1414

15+
#[cfg(feature = "sigs-auth")]
16+
pub mod sigs_auth;
17+
1518
/// Defines a trait around how headers are provided for each VSS request.
1619
#[async_trait]
1720
pub trait VssHeaderProvider: Send + Sync {

src/headers/sigs_auth.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Provides the [`SigsAuthProvider`].
2+
3+
use crate::headers::{VssHeaderProvider, VssHeaderProviderError};
4+
use async_trait::async_trait;
5+
use bitcoin::hashes::sha256::Hash as Sha256;
6+
use bitcoin::hashes::Hash as _;
7+
use bitcoin::secp256k1::{Message, Secp256k1, SecretKey, SignOnly};
8+
use std::collections::HashMap;
9+
use std::fmt::Write as _;
10+
use std::io::Write as _;
11+
use std::time::SystemTime;
12+
13+
/// A 64-byte constant which, after appending the public key, is signed in order to prove knowledge
14+
/// of the corresponding private key.
15+
pub const SIGNING_CONSTANT: &'static [u8] =
16+
b"VSS Signature Authorizer Signing Salt Constant..................";
17+
18+
fn build_token(secret_key: &SecretKey, secp_ctx: &Secp256k1<SignOnly>) -> String {
19+
let pubkey = secret_key.public_key(secp_ctx);
20+
let old_time = "System time must be at least Jan 1, 1970";
21+
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).expect(old_time).as_secs();
22+
23+
// 2^64 serialized as a string is 20 bytes.
24+
let mut buffer = [0u8; SIGNING_CONSTANT.len() + 33 + 20];
25+
let mut stream = &mut buffer[..];
26+
stream.write_all(SIGNING_CONSTANT).unwrap();
27+
stream.write_all(&pubkey.serialize()).unwrap();
28+
write!(stream, "{now}").unwrap();
29+
let bytes_remaining = stream.len();
30+
let bytes_to_sign = &buffer[..buffer.len() - bytes_remaining];
31+
32+
let hash = Sha256::hash(&bytes_to_sign);
33+
let sig = secp_ctx.sign_ecdsa(&Message::from_digest(hash.to_byte_array()), secret_key);
34+
let mut out = String::with_capacity((33 + 64 + 20) * 2);
35+
write!(&mut out, "{pubkey:x}").unwrap();
36+
for c in sig.serialize_compact() {
37+
write!(&mut out, "{:02x}", c).unwrap();
38+
}
39+
write!(&mut out, "{now}").unwrap();
40+
out
41+
}
42+
43+
/// A simple auth provider which simply proves knowledge of a private key.
44+
///
45+
/// It provides a good default authentication mechanism for testing, or in the case that
46+
/// denial-of-service protection against new-account-flooding is mitigated at another layer
47+
/// (e.g. via Apple DeviceCheck or similar remote attestation technologies).
48+
pub struct SigsAuthProvider {
49+
key: SecretKey,
50+
secp_ctx: Secp256k1<SignOnly>,
51+
default_headers: HashMap<String, String>,
52+
}
53+
54+
impl SigsAuthProvider {
55+
/// Creates a new auth provider which simply proves knowledge of a private key.
56+
///
57+
/// This provides an incredibly simple authentication scheme and allows the server to ensure
58+
/// data for separate clients is kept separate, without any application-specific logic.
59+
///
60+
/// In addition to the automatically-added `Authentication` header, any headers provided in
61+
/// `default_headers` (except an `Authentication` header) will be added to the headers list.
62+
pub fn new(key: SecretKey, default_headers: HashMap<String, String>) -> Self {
63+
SigsAuthProvider { secp_ctx: Secp256k1::signing_only(), key, default_headers }
64+
}
65+
}
66+
67+
#[async_trait]
68+
impl VssHeaderProvider for SigsAuthProvider {
69+
async fn get_headers(
70+
&self, _request: &[u8],
71+
) -> Result<HashMap<String, String>, VssHeaderProviderError> {
72+
// TODO: We might consider not re-signing on every request, but its cheap enough that it
73+
// doesn't really matter
74+
let mut headers = self.default_headers.clone();
75+
headers.insert("Authorization".to_owned(), build_token(&self.key, &self.secp_ctx));
76+
Ok(headers)
77+
}
78+
}

0 commit comments

Comments
 (0)