Cách đây không lâu, tại một công ty Muggle, chúng tôi có một câu hỏi về việc lưu trữ khóa riêng cho các dịch vụ khác nhau. Trong quá trình này, chúng tôi đã tìm ra một số giải pháp không phù hợp lắm vì nhiều lý do khác nhau. Tôi quyết định quay lại với nó một lúc nào đó.
Đối với bản thân tôi, tôi đã nêu bật một số yêu cầu để dịch vụ này hoạt động:
sk
Nếu khóa bị xâm phạm, chúng tôi có thể thu hồi nó.
Trong quá trình làm việc, khi nhận ra nhiệm vụ này cần giải quyết như thế nào, tôi nhấn mạnh thêm một yêu cầu:
Vì key sẽ được sử dụng nhiều lần trong bài viết nên mình sẽ gọi public key pk
để tránh nhầm lẫn và key được chia sẻ là sk
.
Tùy chọn ban đầu là mã hóa khóa trong hàm pbkdf2
, nhưng có vấn đề về cách chia sẻ khóa để truy cập chữ ký vì chúng ta chỉ có một khóa trong quá trình thực hiện thuật toán này.
Tôi tìm thấy hai lựa chọn để giải quyết vấn đề:
Chúng tôi có khóa chính của khóa được mã hóa được lưu trữ trong cơ sở dữ liệu và đã chia sẻ khóa được tạo, dẫn đến khóa gốc. Tôi sẽ không nói rằng tôi thích tùy chọn này vì nếu bạn có quyền truy cập vào cơ sở dữ liệu, khóa pk
rất dễ giải mã.
Chúng tôi tạo một phiên bản riêng của khóa pk
cho mỗi khóa chúng tôi kiểm tra. Tôi cũng không nói rằng tôi thích lựa chọn này lắm.
Vì vậy, khi đi loanh quanh và suy nghĩ về cách làm cho khóa sk trở nên thuận tiện, tôi nhớ rằng khi sử dụng Chia sẻ bí mật Shamir (SSS), bạn có thể làm cho khóa sk
trở nên độc đáo và chỉ chia sẻ một phần của khóa. Phần còn lại sẽ được lưu trữ ở phần phụ trợ trong bảo mật và bạn có thể đưa những phần này cho bất kỳ ai bạn muốn.
Nó sẽ trông như thế này: chúng tôi mã hóa khóa pk bằng khóa do SSS tạo, lưu trữ một phần khóa trong các kho lưu trữ khác nhau và cung cấp một phần khóa cho người dùng dưới dạng sk
. Sau khoảng 10-15 phút, tôi nhận ra một điều đơn giản:
Khi sử dụng SSS, chúng ta không cần mã hóa khóa pk
của mình bằng bất kỳ thứ gì khác vì SSS có thể xử lý nó một chút và theo tôi, giải pháp này là hoàn hảo để lưu trữ khóa PK. Nó luôn được tháo rời thành các phần bằng cách sử dụng các tùy chọn lưu trữ khác nhau, bao gồm cả của người dùng. Nếu cần thu hồi nó, chúng tôi sẽ xóa thông tin chỉ mục của khóa sk
của mình và nhanh chóng tập hợp một thông tin mới.
Trong bài viết này, tôi sẽ không tập trung vào các nguyên tắc của SSS; Tôi đã viết một bài báo ngắn về chủ đề này và nhiều nguyên tắc trong bài viết này sẽ tạo thành nền tảng cho dịch vụ mới của chúng tôi.
Nguyên tắc phục vụ của chúng tôi sẽ như sau:
Người dùng chọn tạo khóa.
Chúng tôi tạo một khóa phù hợp cho dịch vụ. Nó sẽ là chìa khóa pk
của chúng tôi. Nó không bao giờ rời khỏi dịch vụ nói chung.
Bằng cách sử dụng SSS, chúng tôi chia khóa của mình sao cho cần có ba phần của khóa phân tách để khôi phục khóa pk
. Mỗi khóa chia gồm hai phần: x: vị trí khóa của chúng ta y: giá trị cho vị trí này
Chúng tôi đưa phần đầu tiên vào Vault (có thể là bất kỳ dịch vụ nào lưu trữ thông tin nhạy cảm có thể được truy cập qua API).
Phần thứ hai chúng tôi lưu vào cơ sở dữ liệu (Tôi sẽ sử dụng PostgreSQL).
Phần thứ ba chúng tôi lưu một phần vào cơ sở dữ liệu và phần còn lại chúng tôi cung cấp cho người dùng ( sk
). Để sử dụng SK tìm giá trị cần thiết, chúng ta cũng lưu keccak256(sk)
vào cơ sở dữ liệu. Theo tôi biết thì nó vẫn chưa bị hỏng.
Khi người dùng cần ký nội dung nào đó, chúng tôi sẽ thu thập khóa riêng tư từ các phần khác nhau của ứng dụng và ký vào đó.
Cách tiếp cận này có một nhược điểm, nếu quản trị viên khóa sk
mất tất cả các khóa sk
do anh ta tạo ra, chúng tôi không thể khôi phục lại khóa ban đầu. Ngoài ra, bạn có thể tạo bản sao lưu của khóa gốc, nhưng đó là lúc khác =).
Kết quả công việc của tôi là tôi có cấu trúc cơ sở dữ liệu này:
Tôi đã sử dụng ngôn ngữ lập trình Rust với khung Actix-web cho dịch vụ này. Tôi sử dụng chúng mọi lúc tại nơi làm việc, vậy tại sao không?
Như tôi đã nói, cơ sở dữ liệu sẽ là Postgresql vì những lý do sau.
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, } } }
Tôi sẽ thú nhận một chút ở đây: Tôi không phải là nhà toán học. Và trong khi tôi cố gắng tìm càng nhiều thông tin về điều này càng tốt, trên thực tế, đây là mã được điều chỉnh từ bài viết trước của tôi.
Bạn có thể đọc thêm về tính năng này tại đây https://en.wikipedia.org/wiki/Lagrange_polynomial
Cấu trúc này (hoặc lớp, tùy theo cách nào thuận tiện hơn) thực hiện phần quan trọng nhất của quy trình mà chúng tôi đã mô tả hôm nay - chia khóa pk
thành nhiều phần và lắp ráp lại nó.
#[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)); } } }
Ở đây, mọi thứ đều đơn giản nhất có thể; chúng tôi tạo một người dùng có khóa chính để làm việc với các khóa của mình. Điều này được thực hiện để ngăn chặn bất kỳ bên nào khác làm bất cứ điều gì với chìa khóa của chúng tôi. Lý tưởng nhất là khóa này không nên được phân phối dưới bất kỳ hình thức nào.
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, }) }
Kiểm tra người dùng xem người dùng đó có tồn tại hay không, tạo khóa pk
, chia khóa thành nhiều phần và lưu chúng ở những nơi khác nhau.
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, }) }
Cơ chế hoạt động của chức năng này như sau:
Chúng tôi xác minh rằng người yêu cầu quyền truy cập có tất cả các quyền đối với Chia sẻ.
Chúng ta cần key bí mật ở đây vì một lý do rất đơn giản, nếu không có nó chúng ta không thể khôi phục key pk
gốc. Tạo một Chia sẻ bổ sung và cung cấp cho người dùng.
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() }
Ở đây, chúng tôi chỉ cần biết mã định danh của Chia sẻ mà chúng tôi đang thu hồi quyền truy cập. Trong tương lai, nếu tôi tạo một giao diện web thì việc làm việc với nó sẽ dễ dàng hơn. Chúng tôi không cần khóa sk
ở đây vì chúng tôi không khôi phục khóa riêng ở đây.
#[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()), }) }
Đã nhận được tin nhắn, nếu mọi thứ đều ổn, hãy khôi phục khóa riêng và ký vào tin nhắn.
Về cơ bản, các phương pháp chính trong ứng dụng của chúng tôi đã được mô tả; Tôi quyết định tiếc nuối và không đưa toàn bộ mã vào đây. Có GitHub cho điều đó và tất cả mã sẽ có sẵn ở đó =)
Mặc dù đây vẫn là bản nháp của ứng dụng nhưng điều quan trọng cần lưu ý là đây không chỉ là một khái niệm. đó là một dự thảo khả thi và đầy hứa hẹn. Ngoài ra còn có các bài kiểm tra tích hợp trong kho lưu trữ để hiểu cách thức hoạt động của nó. Trong các phần sau, tôi dự định thêm chữ ký của các giao dịch và giúp giới hạn phạm vi sử dụng của chúng. Sau đó tôi có thể tạo một giao diện web và làm cho dự án này trở nên thân thiện với người dùng bình thường.
Sự phát triển này có khá nhiều tiềm năng sử dụng và tôi hy vọng rằng mình có thể tiết lộ ít nhất một phần của nó; Tôi xin lỗi vì những trang mã này và những bình luận thưa thớt. Tôi thích viết mã hơn là giải thích những gì tôi đã viết ở trên. Tôi sẽ cố gắng trở nên tốt hơn, nhưng bây giờ điều này mạnh mẽ hơn tôi.
Ngoài ra, hoan nghênh các ý kiến về mã và PR nếu muốn =)
Lời chúc tốt đẹp nhất đến tất cả mọi người và giữ cho khóa riêng của bạn được an toàn.