visit
sk
If the key Is compromised, we should be able to revoke it.
Since the key will be used many times in the article, I will call the private key pk
to avoid confusion and the key that is shared sk
.
The initial option was to encrypt the key in the pbkdf2
function, but there was a problem with how to share the key to access the signature because we have only one key in the process of this algorithm.
We have a master key of the encrypted key stored in the database and have already shared the generated key, which leads to the original key. I wouldn't say I liked this option because if you get access to the database, the pk
key is easy to decrypt.
We create a separate instance of our pk
key for each key we check. I wouldn't say I like this option very much, either.
So, walking around and thinking about how to make the sk key convenient, I remembered that when using Shamir Secrets Sharing (SSS), you can make the sk
key unique and share only a part of the key. The rest will be stored on the backend in security, and you can give these parts to anyone you want.
It would look like this: we encrypt the pk key with our SSS-generated key, store part of the key in different storages, and give part of the key to the user as sk
. After about 10-15 minutes, I realized one straightforward thing:
When using SSS, we don't need to encrypt our pk
key with anything else because SSS can handle it a little bit, and this solution is perfect for storing PK keys, in my opinion. It is always disassembled into parts using different storage options, including the user's. If it needs to be revoked, we delete the index information of our sk
key and quickly assemble a new one.
In this article, I will not dwell on the principles of SSS; I have already written a short article on this topic and many principles from this article will form the basis of our new service.
We create a suitable key for the service. It will be our pk
key. It never leaves the service as a whole.
Using SSS, we split our key so that three parts of the split key are required to recover the pk
key. Each split key consists of two parts:
x: the position of our key
y: the value for this position
The third part we partially save to the database, and the other part we give to the user (sk
). To use SK to find the value we need, we also save keccak256(sk)
to the database. As far as I know, it has not been broken yet.
This approach has one disadvantage, if the sk
key administrator loses all his sk
keys that were generated by him, we can't restore back the original key. As an option, you can make a backup of the original key, but that's for another time =).
As a result of my work, I have this database structure:
lazy_static! {
static ref PRIME: BigUint = BigUint::from_str_radix(
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141",
16
)
.expect("N parse error");
}
#[derive(Clone, Debug)]
pub struct Share {
pub x: BigUint,
pub y: BigUint,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ShareStore {
pub x: String,
pub y: String,
}
impl From<Share> for ShareStore {
fn from(share: Share) -> Self {
ShareStore {
x: hex::encode(share.x.to_bytes_be()),
y: hex::encode(share.y.to_bytes_be()),
}
}
}
impl From<&Share> for ShareStore {
fn from(share: &Share) -> Self {
ShareStore {
x: hex::encode(share.x.to_bytes_be()),
y: hex::encode(share.y.to_bytes_be()),
}
}
}
pub struct Polynomial {
prime: BigUint,
}
impl Polynomial {
pub(crate) fn new() -> Self {
Polynomial {
prime: PRIME.clone(),
}
}
// Calculates the modular multiplicative inverse of `a` modulo `m` using Fermat's Little Theorem.
fn mod_inverse(&self, a: &BigUint, m: &BigUint) -> BigUint {
a.modpow(&(m - 2u32), m)
}
// Generates a random polynomial of a given degree with the secret as the constant term.
fn random_polynomial(&self, degree: usize, secret: &BigUint) -> Vec<BigUint> {
let mut coefficients = vec![secret.clone()];
for _ in 0..degree {
let index = BigUint::from_bytes_be(generate_random().as_slice());
coefficients.push(index);
}
coefficients
}
// Evaluates a polynomial at a given point `x`, using Horner's method for efficient computation under a prime modulus.
fn evaluate_polynomial(&self, coefficients: &[BigUint], x: &BigUint) -> BigUint {
let mut result = BigUint::zero();
let mut power = BigUint::one();
for coeff in coefficients {
result = (&result + (coeff * &power) % &self.prime) % &self.prime;
power = (&power * x) % &self.prime;
}
result
}
// Generates `num_shares` shares from a secret, using a polynomial of degree `threshold - 1`.
pub fn generate_shares(
&self,
secret: &BigUint,
num_shares: usize,
threshold: usize,
) -> Vec<Share> {
let coefficients = self.random_polynomial(threshold - 1, secret);
let mut shares = vec![];
for _x in 1..=num_shares {
let x = BigUint::from_bytes_be(generate_random().as_slice());
let y = self.evaluate_polynomial(&coefficients, &x);
shares.push(Share { x, y });
}
shares
}
// Reconstructs the secret from a subset of shares using Lagrange interpolation in a finite field.
pub fn reconstruct_secret(&self, shares: &Vec<Share>) -> BigUint {
let mut secret = BigUint::zero();
for share_i in shares {
let mut numerator = BigUint::one();
let mut denominator = BigUint::one();
for share_j in shares {
if share_i.x != share_j.x {
numerator = (&numerator * &share_j.x) % &self.prime;
let diff = if share_j.x > share_i.x {
&share_j.x - &share_i.x
} else {
&self.prime - (&share_i.x - &share_j.x)
};
denominator = (&denominator * &diff) % &self.prime;
}
}
let lagrange = (&share_i.y * &numerator * self.mod_inverse(&denominator, &self.prime))
% &self.prime;
secret = (&secret + &lagrange) % &self.prime;
}
secret
}
// Adds a new share to the existing set of shares using Lagrange interpolation in a finite field.
pub fn add_share(&self, shares: &Vec<Share>) -> Share {
let new_index = BigUint::from_bytes_be(generate_random().as_slice());
let mut result = BigUint::zero();
for share_i in shares {
let mut lambda = BigUint::one();
for share_j in shares {
if share_i.x != share_j.x {
let numerator = if new_index.clone() >= share_j.x {
(new_index.clone() - &share_j.x) % &self.prime
} else {
(&self.prime - (&share_j.x - new_index.clone()) % &self.prime) % &self.prime
};
let denominator = if share_i.x >= share_j.x {
(&share_i.x - &share_j.x) % &self.prime
} else {
(&self.prime - (&share_j.x - &share_i.x) % &self.prime) % &self.prime
};
lambda = (&lambda * &numerator * self.mod_inverse(&denominator, &self.prime))
% &self.prime;
}
}
result = (&result + &share_i.y * &lambda) % &self.prime;
}
Share {
x: new_index,
y: result,
}
}
}
I'll make a bit of a confession here: I'm not a mathematician. And while I tried to find as much information about this as I could, in fact, this is adapted code from my previous article.
This structure (or class, whichever is more convenient) performs the most important part of the process we described today - breaking the pk
key into pieces and reassembling it again.
#[derive(Serialize, Deserialize)]
pub struct CreateUserResponse {
pub secret: String,
}
pub async fn users_create_handler(app_data: web::Data<AppData>) -> HttpResponse {
let code = generate_code();
match create_user(
CreateOrUpdateUser {
secret: code.clone(),
},
app_data.get_db_connection(),
)
.await
{
Ok(_) => HttpResponse::Ok().json(CreateUserResponse { secret: code }),
Err(e) => {
return HttpResponse::InternalServerError().body(format!("Error creating user: {}", e));
}
}
}
Here, everything is as simple as possible; we create a user who has a master key to work with his keys. This is done to prevent any other party from doing anything with our keys. Ideally, this key should not be distributed in any way.
pub async fn keys_generate_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse {
// Check if the request has a master key header
let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else {
return HttpResponse::Unauthorized().finish();
};
// Check if user with master key exist
let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await {
Ok(user) => user,
Err(UserErrors::NotFound(_)) => {
return HttpResponse::Unauthorized().finish();
}
Err(e) => {
return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e));
}
};
// generate random `pk` private key
let private_key = generate_random();
let Ok(signer) = PrivateKeySigner::from_slice(private_key.as_slice()) else {
return HttpResponse::InternalServerError().finish();
};
let secret = BigUint::from_bytes_be(private_key.as_slice());
let poly = Polynomial::new();
// divide `pk` key into 3 shares
let shares = poly
.generate_shares(&secret, 3, 3)
.iter()
.map(Into::into)
.collect::<Vec<ShareStore>>();
// store first part at Vault
let path = generate_code();
if let Err(err) = kv2::set(
app_data.get_vault_client().as_ref(),
"secret",
&path,
&shares[0],
)
.await
{
return HttpResponse::InternalServerError().body(format!("Error setting secret: {}", err));
}
// Store second part at database and path to first share
let key = CreateOrUpdateKey {
user_id: user.id,
local_key: shares[1].y.clone(),
local_index: shares[1].y.clone(),
cloud_key: path,
address: signer.address(),
};
let key = match create_key(key, app_data.get_db_connection()).await {
Ok(key) => key,
Err(err) => {
return HttpResponse::InternalServerError()
.body(format!("Error creating key: {}", err));
}
};
// Store third part at database as share
let share = match create_share(
CreateOrUpdateShare {
secret: shares[2].y.clone(),
key_id: key.id,
user_index: shares[2].x.clone(),
owner: SharesOwner::Admin,
},
app_data.get_db_connection(),
)
.await
{
Ok(share) => share,
Err(err) => {
return HttpResponse::InternalServerError()
.body(format!("Error creating share: {}", err));
}
};
let Ok(user_key) = hex::decode(&shares[2].y) else {
return HttpResponse::InternalServerError().finish();
};
// Store log
let _ = create_log(
CreateLog {
key_id: key.id,
action: "generate_key".to_string(),
data: serde_json::json!({
"user_id": user.id
}),
message: None,
},
app_data.get_db_connection(),
)
.await;
// Return the key and share identifier
HttpResponse::Ok().json(KeysGenerateResponse {
key: STANDARD.encode(user_key),
id: share.id,
})
}
Check the user that such a user exists, create a pk
key, split it into parts, and save them in different places.
pub async fn keys_grant_handler(req: HttpRequest, app_data: web::Data<AppData>) -> HttpResponse {
// Check if the request has a master key header
let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else {
return HttpResponse::Unauthorized().finish();
};
// Check if a user with the master key exists
let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await {
Ok(user) => user,
Err(UserErrors::NotFound(_)) => {
return HttpResponse::Unauthorized().finish();
}
Err(e) => {
return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e));
}
};
// Check if the request has a secret key header
let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else {
return HttpResponse::Unauthorized().finish();
};
let Ok(share) = STANDARD.decode(secret_key) else {
return HttpResponse::Unauthorized().finish();
};
// Check if the share exists
let share_value = hex::encode(share);
let share = match get_share_by_secret(&share_value, app_data.get_db_connection()).await {
Ok(share) => share,
Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(),
Err(_) => {
return HttpResponse::Unauthorized().finish();
}
};
if !matches!(share.status, SharesStatus::Granted) {
return HttpResponse::Unauthorized().finish();
}
// Get original key with necessary information
let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await {
Ok(key) => key,
Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(),
Err(_) => {
return HttpResponse::Unauthorized().finish();
}
};
// Check if the key belongs to the user
if key.user_id != user.id {
return HttpResponse::Unauthorized().finish();
}
// Get the first part of the key from Vault
let Ok(cloud_secret) = kv2::read::<ShareStore>(
app_data.get_vault_client().as_ref(),
"secret",
&key.cloud_key,
)
.await
else {
return HttpResponse::InternalServerError().finish();
};
// Combine the shares
let shares = vec![
Share {
x: BigUint::from_str_radix(&cloud_secret.x, 16).expect("Error parsing local index"),
y: BigUint::from_str_radix(&cloud_secret.y, 16).expect("Error parsing local key"),
},
Share {
x: BigUint::from_str_radix(&key.local_index, 16).expect("Error parsing local index"),
y: BigUint::from_str_radix(&key.local_key, 16).expect("Error parsing local key"),
},
Share {
x: BigUint::from_str_radix(&share.user_index, 16).expect("Error parsing user index"),
y: BigUint::from_str_radix(&share_value, 16).expect("Error parsing user key"),
},
];
let sss = Polynomial::new();
// Create a new share
let new_share = ShareStore::from(sss.add_share(&shares));
// Store new share into database
let share = match create_share(
CreateOrUpdateShare {
secret: new_share.y.to_string(),
key_id: key.id,
user_index: new_share.x.to_string(),
owner: SharesOwner::Guest,
},
app_data.get_db_connection(),
)
.await
{
Ok(share) => share,
Err(err) => {
return HttpResponse::InternalServerError()
.body(format!("Error creating share: {}", err));
}
};
let Ok(user_key) = hex::decode(&new_share.y).map(|k| STANDARD.encode(k)) else {
return HttpResponse::InternalServerError().finish();
};
// Store log
let _ = create_log(
CreateLog {
key_id: key.id,
action: "grant".to_string(),
data: serde_json::json!({
"user_id": user.id,
"share_id": share.id,
}),
message: None,
},
app_data.get_db_connection(),
)
.await;
// Return the key and share the identifier
HttpResponse::Ok().json(KeysGenerateResponse {
key: user_key,
id: share.id,
})
}
The mechanism of operation of this function is as follows:
We need the secret key here for a very simple reason, without it we cannot recover the original pk
key. Create an additional Share, and give it to the user.
pub async fn keys_revoke_handler(
req: HttpRequest,
app_data: web::Data<AppData>,
body: web::Json<KeysRevokeRequest>,
) -> HttpResponse {
let Some(Ok(master_key)) = req.headers().get(MASTER_KEY).map(|header| header.to_str()) else {
return HttpResponse::Unauthorized().finish();
};
let user = match get_user_by_secret(&master_key, app_data.get_db_connection()).await {
Ok(user) => user,
Err(UserErrors::NotFound(_)) => {
return HttpResponse::Unauthorized().finish();
}
Err(e) => {
return HttpResponse::InternalServerError().body(format!("Error getting user: {}", e));
}
};
let share = match get_share_by_id(&body.id, app_data.get_db_connection()).await {
Ok(share) => share,
Err(ShareErrors::NotFound(_)) => return HttpResponse::NotFound().finish(),
Err(_) => {
return HttpResponse::Unauthorized().finish();
}
};
let key = match get_key_by_id(&share.key_id, app_data.get_db_connection()).await {
Ok(key) => key,
Err(KeyErrors::NotFound(_)) => return HttpResponse::NotFound().finish(),
Err(_) => {
return HttpResponse::Unauthorized().finish();
}
};
if key.user_id != user.id {
return HttpResponse::Unauthorized().finish();
}
if revoke_share_by_id(&share.id, app_data.get_db_connection())
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
let _ = create_log(
CreateLog {
key_id: key.id,
action: "revoke".to_string(),
data: serde_json::json!({
"user_id": user.id,
"share_id": share.id,
}),
message: None,
},
app_data.get_db_connection(),
)
.await;
HttpResponse::Ok().finish()
}
Here, we only need to know the identifier of the Share to which we are revoking access. In the future, if I do make a web interface, this will be easier to work with. We don't need our sk
key here because we are not restoring the private key here.
#[derive(Deserialize, Serialize, Debug)]
pub struct SignMessageRequest {
pub message: String,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct SignMessageResponse {
pub signature: String,
}
pub async fn sign_message_handler(
app_data: web::Data<AppData>,
req: HttpRequest,
body: web::Json<SignMessageRequest>,
) -> HttpResponse {
// Get the `sk` key from the request headers
let Some(Ok(secret_key)) = req.headers().get(SECRET_KEY).map(|header| header.to_str()) else {
return HttpResponse::Unauthorized().finish();
};
// restore shares
let (shares, key_id, share_id) = match restore_shares(secret_key, &app_data).await {
Ok(shares) => shares,
Err(e) => {
return HttpResponse::BadRequest().json(json!({"error": e.to_string()}));
}
};
let sss = Polynomial::new();
// restore `pk` key
let private_key = sss.reconstruct_secret(&shares);
//sign message
let Ok(signer) = PrivateKeySigner::from_slice(private_key.to_bytes_be().as_slice()) else {
return HttpResponse::InternalServerError().finish();
};
let Ok(signature) = signer.sign_message(body.message.as_bytes()).await else {
return HttpResponse::InternalServerError().finish();
};
// create log
let _ = create_log(
CreateLog {
key_id,
action: "sign_message".to_string(),
data: json!({
"share_id": share_id,
}),
message: Some(body.message.clone()),
},
app_data.get_db_connection(),
)
.await;
// return signature
HttpResponse::Ok().json(SignMessageResponse {
signature: hex::encode(signature.as_bytes()),
})
}
Received the message, if everything was ok, recovered the private key, and signed the message with it.