use eth2::lighthouse_vc::{PK_LEN, SECRET_PREFIX as PK_PREFIX}; use filesystem::create_with_600_perms; use libsecp256k1::{Message, PublicKey, SecretKey}; use rand::thread_rng; use ring::digest::{digest, SHA256}; use std::fs; use std::path::{Path, PathBuf}; use warp::Filter; /// The name of the file which stores the secret key. /// /// It is purposefully opaque to prevent users confusing it with the "secret" that they need to /// share with API consumers (which is actually the public key). pub const SK_FILENAME: &str = ".secp-sk"; /// Length of the raw secret key, in bytes. pub const SK_LEN: usize = 32; /// The name of the file which stores the public key. /// /// For users, this public key is a "secret" that can be shared with API consumers to provide them /// access to the API. We avoid calling it a "public" key to users, since they should not post this /// value in a public forum. pub const PK_FILENAME: &str = "api-token.txt"; /// Contains a `secp256k1` keypair that is saved-to/loaded-from disk on instantiation. The keypair /// is used for authorization/authentication for requests/responses on the HTTP API. /// /// Provides convenience functions to ultimately provide: /// /// - A signature across outgoing HTTP responses, applied to the `Signature` header. /// - Verification of proof-of-knowledge of the public key in `self` for incoming HTTP requests, /// via the `Authorization` header. /// /// The aforementioned scheme was first defined here: /// /// https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855 pub struct ApiSecret { pk: PublicKey, sk: SecretKey, pk_path: PathBuf, } impl ApiSecret { /// If both the secret and public keys are already on-disk, parse them and ensure they're both /// from the same keypair. /// /// The provided `dir` is a directory containing two files, `SK_FILENAME` and `PK_FILENAME`. /// /// If either the secret or public key files are missing on disk, create a new keypair and /// write it to disk (over-writing any existing files). pub fn create_or_open>(dir: P) -> Result { let sk_path = dir.as_ref().join(SK_FILENAME); let pk_path = dir.as_ref().join(PK_FILENAME); if !(sk_path.exists() && pk_path.exists()) { let sk = SecretKey::random(&mut thread_rng()); let pk = PublicKey::from_secret_key(&sk); // Create and write the secret key to file with appropriate permissions create_with_600_perms( &sk_path, serde_utils::hex::encode(sk.serialize()).as_bytes(), ) .map_err(|e| { format!( "Unable to create file with permissions for {:?}: {:?}", sk_path, e ) })?; // Create and write the public key to file with appropriate permissions create_with_600_perms( &pk_path, format!( "{}{}", PK_PREFIX, serde_utils::hex::encode(&pk.serialize_compressed()[..]) ) .as_bytes(), ) .map_err(|e| { format!( "Unable to create file with permissions for {:?}: {:?}", pk_path, e ) })?; } let sk = fs::read(&sk_path) .map_err(|e| format!("cannot read {}: {}", SK_FILENAME, e)) .and_then(|bytes| { serde_utils::hex::decode(&String::from_utf8_lossy(&bytes)) .map_err(|_| format!("{} should be 0x-prefixed hex", PK_FILENAME)) }) .and_then(|bytes| { if bytes.len() == SK_LEN { let mut array = [0; SK_LEN]; array.copy_from_slice(&bytes); SecretKey::parse(&array).map_err(|e| format!("invalid {}: {}", SK_FILENAME, e)) } else { Err(format!( "{} expected {} bytes not {}", SK_FILENAME, SK_LEN, bytes.len() )) } })?; let pk = fs::read(&pk_path) .map_err(|e| format!("cannot read {}: {}", PK_FILENAME, e)) .and_then(|bytes| { let hex = String::from_utf8(bytes).map_err(|_| format!("{} is not utf8", SK_FILENAME))?; if let Some(stripped) = hex.strip_prefix(PK_PREFIX) { serde_utils::hex::decode(stripped) .map_err(|_| format!("{} should be 0x-prefixed hex", SK_FILENAME)) } else { Err(format!("unable to parse {}", SK_FILENAME)) } }) .and_then(|bytes| { if bytes.len() == PK_LEN { let mut array = [0; PK_LEN]; array.copy_from_slice(&bytes); PublicKey::parse_compressed(&array) .map_err(|e| format!("invalid {}: {}", PK_FILENAME, e)) } else { Err(format!( "{} expected {} bytes not {}", PK_FILENAME, PK_LEN, bytes.len() )) } })?; // Ensure that the keys loaded from disk are indeed a pair. if PublicKey::from_secret_key(&sk) != pk { fs::remove_file(&sk_path) .map_err(|e| format!("unable to remove {}: {}", SK_FILENAME, e))?; fs::remove_file(&pk_path) .map_err(|e| format!("unable to remove {}: {}", PK_FILENAME, e))?; return Err(format!( "{:?} does not match {:?} and the files have been deleted. Please try again.", sk_path, pk_path )); } Ok(Self { pk, sk, pk_path }) } /// Returns the public key of `self` as a 0x-prefixed hex string. fn pubkey_string(&self) -> String { serde_utils::hex::encode(&self.pk.serialize_compressed()[..]) } /// Returns the API token. pub fn api_token(&self) -> String { format!("{}{}", PK_PREFIX, self.pubkey_string()) } /// Returns the path for the API token file pub fn api_token_path(&self) -> PathBuf { self.pk_path.clone() } /// Returns the values of the `Authorization` header which indicate a valid incoming HTTP /// request. /// /// For backwards-compatibility we accept the token in a basic authentication style, but this is /// technically invalid according to RFC 7617 because the token is not a base64-encoded username /// and password. As such, bearer authentication should be preferred. fn auth_header_values(&self) -> Vec { vec![ format!("Basic {}", self.api_token()), format!("Bearer {}", self.api_token()), ] } /// Returns a `warp` header which filters out request that have a missing or inaccurate /// `Authorization` header. pub fn authorization_header_filter(&self) -> warp::filters::BoxedFilter<()> { let expected = self.auth_header_values(); warp::any() .map(move || expected.clone()) .and(warp::filters::header::header("Authorization")) .and_then(move |expected: Vec, header: String| async move { if expected.contains(&header) { Ok(()) } else { Err(warp_utils::reject::invalid_auth(header)) } }) .untuple_one() .boxed() } /// Returns a closure which produces a signature over some bytes using the secret key in /// `self`. The signature is a 32-byte hash formatted as a 0x-prefixed string. pub fn signer(&self) -> impl Fn(&[u8]) -> String + Clone { let sk = self.sk; move |input: &[u8]| -> String { let message = Message::parse_slice(digest(&SHA256, input).as_ref()).expect("sha256 is 32 bytes"); let (signature, _) = libsecp256k1::sign(&message, &sk); serde_utils::hex::encode(signature.serialize_der().as_ref()) } } }