Skip to content

Commit e9222c5

Browse files
committed
Move all of the backup key stuff into a module.
This is an intermediate state. Eventually the Hsm type won't touch any of they backup key creation and splitting. The main module will take over that part and the Hsm module will continue to deminish.
1 parent 00400b9 commit e9222c5

9 files changed

+286
-260
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ vsss-rs = "2.7.1"
3636
x509-cert = "0.2.5"
3737
yubihsm = { git = "https://github.com/oxidecomputer/yubihsm.rs", branch = "session-close", features = ["usb", "untested"] }
3838
zeroize = "1.8.1"
39+
zeroize_derive = "1.4.2"

src/backup.rs

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use anyhow::Result;
6+
use log::{debug, info};
7+
use p256::{
8+
elliptic_curve::PrimeField, NonZeroScalar, ProjectivePoint, Scalar,
9+
SecretKey,
10+
};
11+
use rand_core::{CryptoRng, RngCore};
12+
use std::ops::Deref;
13+
use vsss_rs::{Feldman, FeldmanVerifier};
14+
use zeroize::{DefaultIsZeroes, Zeroizing};
15+
16+
pub const KEY_LEN: usize = 32;
17+
const SHARE_LEN: usize = KEY_LEN + 1;
18+
19+
pub const LIMIT: usize = 5;
20+
pub const THRESHOLD: usize = 3;
21+
static_assertions::const_assert!(THRESHOLD <= LIMIT);
22+
23+
pub type Share = vsss_rs::Share<SHARE_LEN>;
24+
pub type SharesMax = [Share; LIMIT];
25+
pub type Verifier = FeldmanVerifier<Scalar, ProjectivePoint, THRESHOLD>;
26+
27+
/// A key we use to backup keys in the HSM. This type implements operations we
28+
/// perform on / with this key when it's not in the HSM.
29+
#[derive(Clone, Copy, Default)]
30+
pub struct BackupKey([u8; KEY_LEN]);
31+
32+
impl DefaultIsZeroes for BackupKey {}
33+
34+
impl BackupKey {
35+
pub fn from_rng<T: RngCore>(rng: &mut T) -> Result<Self> {
36+
let mut key = [0u8; KEY_LEN];
37+
rng.try_fill_bytes(&mut key)?;
38+
Ok(Self(key))
39+
}
40+
41+
// use as_bytes::AsBytes;
42+
pub fn as_bytes(&self) -> &[u8] {
43+
&self.0
44+
}
45+
46+
// impl From<SharesThreshold> for BackupKey {} or something?
47+
pub fn from_shares(shares: Zeroizing<Vec<Share>>) -> Result<Self> {
48+
let scalar = Feldman::<THRESHOLD, LIMIT>::combine_shares::<
49+
Scalar,
50+
SHARE_LEN,
51+
>(shares.deref())
52+
.map_err(|e| {
53+
anyhow::anyhow!(format!("Failed to combine_shares: {}", e))
54+
})?;
55+
56+
let nz_scalar = NonZeroScalar::from_repr(scalar.to_repr());
57+
let nz_scalar = if nz_scalar.is_some().into() {
58+
nz_scalar.unwrap()
59+
} else {
60+
return Err(anyhow::anyhow!(
61+
"Failed to construct NonZeroScalar from Scalar"
62+
));
63+
};
64+
65+
// not sure this is necessary ... can we just get it from the Scalar?
66+
let wrap_key = SecretKey::from(nz_scalar);
67+
68+
//let foo: [u8; KEY_LEN] = wrap_key.to_be_bytes().try_into()?;
69+
70+
Ok(Self(wrap_key.to_be_bytes().into()))
71+
}
72+
73+
pub fn split<R: CryptoRng + RngCore>(
74+
&self,
75+
rng: &mut R,
76+
) -> Result<(Zeroizing<SharesMax>, Verifier)> {
77+
info!("Splitting wrap key into {} shares.", LIMIT);
78+
let wrap_key =
79+
SecretKey::from_be_bytes(self.as_bytes()).map_err(|e| {
80+
anyhow::anyhow!("Failed to construct SecretKey: {}", e)
81+
})?;
82+
debug!("wrap key: {:?}", wrap_key.to_be_bytes());
83+
84+
let nzs = wrap_key.to_nonzero_scalar();
85+
let (shares, verifier) = Feldman::<THRESHOLD, LIMIT>::split_secret::<
86+
Scalar,
87+
ProjectivePoint,
88+
R,
89+
SHARE_LEN,
90+
>(*nzs.as_ref(), None, &mut *rng)
91+
.map_err(|e| anyhow::anyhow!("Failed to split_secret: {}", e))?;
92+
93+
Ok((Zeroizing::new(shares), verifier))
94+
}
95+
}
96+
97+
#[cfg(test)]
98+
mod tests {
99+
use super::*;
100+
use anyhow::Context;
101+
102+
// secret split into the feldman verifier & shares below
103+
const SECRET: &str =
104+
"f259a45c17624b9317d8e292050c46a0f3d7387724b4cd26dd94f8bd3d1c0e1a";
105+
106+
// verifier created and serialized to json by `new_split_wrap`
107+
const VERIFIER: &str = r#"
108+
{
109+
"generator": "036b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296",
110+
"commitments": [
111+
"022f65c477affe7de97a51b8e562e763030218a8f0a8ecd7c349a50df7ded44985",
112+
"03365076080ebeeab74e2421fa0f4e4c5796ad3cbd157cc0405b100a45ae89f22f",
113+
"02bbd29359d702ff89ab2cbdb9e6ae102dfb1c4108aeab0701a469f28f0ad1e813"
114+
]
115+
}"#;
116+
117+
// shares dumped to the printer by `new_split_wrap`
118+
const SHARE_ARRAY: [&str; LIMIT] = [
119+
"01a69b62eb1a7c9deb5435ca73bf6f5e280279ba9cbdcd873d4decb665fb8aaf34",
120+
"020495513aa59e274196125218ff57b2f01f6bf97d817d24a1a00c5fbf29af08a8",
121+
"030c476f49b8c6e796dd6e7981c4c544f90794efc716db43d8c7adbf8bc3ec3fc7",
122+
"04bdb1bd1853f6deeb2a4a40ae0fb81442baf49d797de7e4e2c4d0d5cbca425491",
123+
"0518d43aa8772e0d3c7ca5a79de03020cdbfbd0d396873cab5b0020cf943eafc64",
124+
];
125+
126+
fn secret_bytes() -> [u8; KEY_LEN] {
127+
let mut secret = [0u8; KEY_LEN];
128+
hex::decode_to_slice(SECRET, &mut secret).unwrap();
129+
130+
secret
131+
}
132+
133+
fn deserialize_share(share: &str) -> Result<Share> {
134+
// filter out whitespace to keep hex::decode happy
135+
let share: String =
136+
share.chars().filter(|c| !c.is_whitespace()).collect();
137+
let share = hex::decode(share)
138+
.context("failed to decode share from hex string")?;
139+
140+
Ok(Share::try_from(&share[..])
141+
.context("Failed to construct Share from bytes.")?)
142+
}
143+
144+
#[test]
145+
fn round_trip() -> Result<()> {
146+
use rand::rngs::ThreadRng;
147+
148+
let secret = secret_bytes();
149+
let secret_key = SecretKey::from_be_bytes(&secret)?;
150+
let nzs = secret_key.to_nonzero_scalar();
151+
152+
let mut rng = ThreadRng::default();
153+
let (shares, verifier) = Feldman::<THRESHOLD, LIMIT>::split_secret::<
154+
Scalar,
155+
ProjectivePoint,
156+
ThreadRng,
157+
SHARE_LEN,
158+
>(*nzs.as_ref(), None, &mut rng)
159+
.map_err(|e| anyhow::anyhow!("failed to split secret: {}", e))?;
160+
161+
for s in &shares {
162+
assert!(verifier.verify(s));
163+
}
164+
165+
let scalar = Feldman::<THRESHOLD, LIMIT>::combine_shares::<
166+
Scalar,
167+
SHARE_LEN,
168+
>(&shares)
169+
.map_err(|e| anyhow::anyhow!("failed to combine secret: {}", e))?;
170+
171+
let nzs_dup = NonZeroScalar::from_repr(scalar.to_repr()).unwrap();
172+
let sk_dup = SecretKey::from(nzs_dup);
173+
let new_secret: [u8; KEY_LEN] = sk_dup.to_be_bytes().try_into()?;
174+
175+
assert_eq!(new_secret, secret);
176+
177+
Ok(())
178+
}
179+
180+
// deserialize a verifier & use it to verify the shares in SHARE_ARRAY
181+
#[test]
182+
fn verify_shares() -> Result<()> {
183+
let verifier: Verifier = serde_json::from_str(VERIFIER)
184+
.context("Failed to deserialize Verifier from JSON.")?;
185+
186+
for share in SHARE_ARRAY {
187+
let share = deserialize_share(share)?;
188+
assert!(verifier.verify(&share));
189+
}
190+
191+
Ok(())
192+
}
193+
194+
#[test]
195+
fn verify_zero_share() -> Result<()> {
196+
let verifier: Verifier = serde_json::from_str(VERIFIER)
197+
.context("Failed to deserialize FeldmanVerifier from JSON.")?;
198+
199+
let share = Share::try_from([0u8; SHARE_LEN].as_ref())
200+
.context("Failed to create Share from static array.")?;
201+
202+
assert!(!verifier.verify(&share));
203+
204+
Ok(())
205+
}
206+
207+
// TODO: I had expected that changing a single bit in a share would case
208+
// the verifier to fail but that seems to be very wrong.
209+
#[test]
210+
fn verify_share_with_changed_byte() -> Result<()> {
211+
let verifier: Verifier = serde_json::from_str(VERIFIER)
212+
.context("Failed to deserialize FeldmanVerifier from JSON.")?;
213+
214+
let mut share = deserialize_share(SHARE_ARRAY[0])?;
215+
println!("share: {}", share.0[0]);
216+
share.0[1] = 0xff;
217+
share.0[2] = 0xff;
218+
share.0[3] = 0xff;
219+
// If we don't change the next byte this test will start failing.
220+
// I had (wrongly?) expected that the share would fail to verify w/
221+
// a single changed byte
222+
share.0[4] = 0xff;
223+
224+
assert!(!verifier.verify(&share));
225+
226+
Ok(())
227+
}
228+
229+
#[test]
230+
fn recover_secret() -> Result<()> {
231+
let mut shares: Vec<Share> = Vec::new();
232+
for share in SHARE_ARRAY {
233+
shares.push(deserialize_share(share)?);
234+
}
235+
236+
let scalar = Feldman::<THRESHOLD, LIMIT>::combine_shares::<
237+
Scalar,
238+
SHARE_LEN,
239+
>(&shares)
240+
.map_err(|e| anyhow::anyhow!("failed to combine secret: {}", e))?;
241+
242+
let nzs_dup = NonZeroScalar::from_repr(scalar.to_repr()).unwrap();
243+
let sk_dup = SecretKey::from(nzs_dup);
244+
let secret: [u8; KEY_LEN] = sk_dup.to_be_bytes().try_into()?;
245+
246+
assert_eq!(secret, secret_bytes());
247+
248+
Ok(())
249+
}
250+
251+
#[test]
252+
fn from_rng() -> Result<()> {
253+
let mut rng = rand::thread_rng();
254+
let backup_key = BackupKey::from_rng(&mut rng);
255+
256+
assert!(backup_key.is_ok());
257+
258+
Ok(())
259+
}
260+
}

src/bin/printer-test.rs

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ use std::path::PathBuf;
77
use anyhow::Result;
88
use clap::{Parser, Subcommand};
99
use hex::ToHex;
10-
use oks::{
11-
hsm::{Alphabet, Share},
12-
secret_writer::PrinterSecretWriter,
13-
};
10+
use oks::{backup::Share, hsm::Alphabet, secret_writer::PrinterSecretWriter};
1411
use rand::{thread_rng, Rng};
1512
use zeroize::Zeroizing;
1613

0 commit comments

Comments
 (0)