Il n'y a pas si longtemps, lors d'un travail moldu, nous avons eu une question sur le stockage des clés privées pour divers services. Au cours du processus, nous avons trouvé plusieurs solutions qui n’étaient pas très adaptées pour différentes raisons. J'ai décidé d'y revenir un jour.
Pour ma part, j'ai souligné quelques exigences pour que ce service fonctionne :
sk
Si la clé est compromise, nous devrions pouvoir la révoquer.
Déjà en cours de travail, lorsque j'ai réalisé comment cette tâche devait être résolue, j'ai souligné une exigence supplémentaire :
Puisque la clé sera utilisée plusieurs fois dans l'article, j'appellerai la clé privée pk
pour éviter toute confusion et la clé partagée sk
.
L'option initiale était de chiffrer la clé dans la fonction pbkdf2
, mais il y avait un problème avec la façon de partager la clé pour accéder à la signature car nous n'avons qu'une seule clé dans le processus de cet algorithme.
J'ai trouvé deux options pour résoudre le problème :
Nous disposons d'une clé principale de la clé cryptée stockée dans la base de données et avons déjà partagé la clé générée, qui mène à la clé d'origine. Je ne dirais pas que j'ai aimé cette option car si vous avez accès à la base de données, la clé pk
est facile à déchiffrer.
Nous créons une instance distincte de notre clé pk
pour chaque clé que nous vérifions. Je ne dirais pas non plus que j’aime beaucoup cette option.
Ainsi, en me promenant et en réfléchissant à la façon de rendre la clé sk pratique, je me suis souvenu qu'en utilisant Shamir Secrets Sharing (SSS), vous pouvez rendre la clé sk
unique et partager seulement une partie de la clé. Le reste sera stocké sur le backend en toute sécurité et vous pourrez donner ces pièces à qui vous voulez.
Cela ressemblerait à ceci : nous chiffrons la clé pk avec notre clé générée par SSS, stockons une partie de la clé dans différents stockages et donnons une partie de la clé à l'utilisateur sous le nom sk
. Après environ 10 à 15 minutes, j'ai réalisé une chose simple :
Lorsque nous utilisons SSS, nous n'avons pas besoin de chiffrer notre clé pk
avec autre chose car SSS peut la gérer un peu, et cette solution est parfaite pour stocker les clés PK, à mon avis. Il est toujours démonté en plusieurs parties en utilisant différentes options de stockage, y compris celle de l'utilisateur. S'il doit être révoqué, nous supprimons les informations d'index de notre clé sk
et en assemblons rapidement une nouvelle.
Dans cet article, je ne m'étendrai pas sur les principes du SSS ; J'ai déjà écrit un court article sur ce sujet et de nombreux principes de cet article constitueront la base de notre nouveau service.
Le principe de notre prestation sera le suivant :
L'utilisateur choisit de générer une clé.
Nous créons une clé adaptée au service. Ce sera notre clé pk
. Il ne quitte jamais le service dans son ensemble.
En utilisant SSS, nous divisons notre clé de sorte que trois parties de la clé divisée soient nécessaires pour récupérer la clé pk
. Chaque clé partagée se compose de deux parties : x : la position de notre clé y : la valeur de cette position
Nous jetons la première partie dans Vault (il peut s'agir de n'importe quel service de stockage d'informations sensibles accessible via l'API).
La deuxième partie que nous enregistrons dans la base de données (je vais utiliser PostgreSQL).
La troisième partie est partiellement enregistrée dans la base de données et l'autre partie est donnée à l'utilisateur ( sk
). Pour utiliser SK afin de trouver la valeur dont nous avons besoin, nous enregistrons également keccak256(sk)
dans la base de données. A ma connaissance, il n'est pas encore cassé.
Lorsque l'utilisateur a besoin de signer quelque chose, nous collectons la clé privée de différentes parties de l'application et la signons.
Cette approche présente un inconvénient : si l'administrateur de clé sk
perd toutes ses clés sk
qu'il a générées, nous ne pouvons pas restaurer la clé d'origine. En option, vous pouvez faire une sauvegarde de la clé originale, mais c'est pour une autre fois =).
Grâce à mon travail, j'ai cette structure de base de données :
J'ai utilisé le langage de programmation Rust avec le framework Actix-web pour ce service. Je les utilise tout le temps au travail, alors pourquoi pas ?
Comme je l'ai dit, la base de données sera Postgresql pour les raisons suivantes.
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, } } }
Je vais faire un petit aveu ici : je ne suis pas mathématicien. Et même si j’essayais de trouver autant d’informations que possible à ce sujet, il s’agit en fait d’un code adapté de mon article précédent.
Vous pouvez en savoir plus sur cette fonctionnalité ici https://en.wikipedia.org/wiki/Lagrange_polynomial
Cette structure (ou classe, selon ce qui est le plus pratique) effectue la partie la plus importante du processus que nous avons décrit aujourd'hui : briser la clé pk
en morceaux et la réassembler à nouveau.
#[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)); } } }
Ici, tout est le plus simple possible ; nous créons un utilisateur qui dispose d'un passe-partout pour travailler avec ses clés. Ceci est fait pour empêcher toute autre partie de faire quoi que ce soit avec nos clés. Idéalement, cette clé ne devrait en aucun cas être distribuée.
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, }) }
Vérifiez auprès de l'utilisateur qu'un tel utilisateur existe, créez une clé pk
, divisez-la en parties et enregistrez-les à différents endroits.
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, }) }
Le mécanisme de fonctionnement de cette fonction est le suivant :
Nous vérifions que le demandeur d'accès dispose de tous les droits sur le Partage.
Nous avons besoin de la clé secrète ici pour une raison très simple, sans elle nous ne pouvons pas récupérer la clé pk
originale. Créez un partage supplémentaire et donnez-le à l'utilisateur.
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() }
Ici, il suffit de connaître l'identifiant du partage auquel nous révoquons l'accès. À l’avenir, si je crée une interface Web, elle sera plus facile à utiliser. Nous n'avons pas besoin de notre clé sk
ici car nous ne restaurons pas la clé privée ici.
#[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()), }) }
Reçu le message, si tout allait bien, récupéré la clé privée et signé le message avec.
Fondamentalement, les principales méthodes de notre application sont décrites ; J'ai décidé d'avoir pitié et de ne pas mettre tout le code ici. Il y a GitHub pour ça, et tout le code y sera disponible =)
Bien qu’il s’agisse encore d’une ébauche de l’application, il est important de noter qu’il ne s’agit pas simplement d’un concept. c'est un projet réalisable et prometteur. Il existe également des tests d'intégration dans le référentiel pour comprendre son fonctionnement. Dans les parties suivantes, je prévois d'ajouter la signature des transactions et de permettre de limiter la portée de leur utilisation. Je pourrai ensuite créer une interface Web et rendre ce projet convivial pour le commun des mortels.
Ce développement a un grand potentiel d'utilisation, et j'espère pouvoir en révéler au moins une partie ; Je m'excuse pour ces pages de code et ces commentaires clairsemés. Je préfère écrire du code plutôt que d'expliquer ce que j'ai écrit ci-dessus. Je vais essayer de m'améliorer, mais c'est plus fort maintenant que moi.
Aussi, n'hésitez pas à commenter le code et les relations publiques si vous le souhaitez =)
Meilleurs vœux à tous et gardez vos clés privées en sécurité.