-
Notifications
You must be signed in to change notification settings - Fork 183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
minimum self-signed DICE #698
Conversation
43d36a9
to
24f50bb
Compare
NOTE: In addressing review comments I noticed that I hadn't enabled dice in the app toml until the commit with the demo task (that should not be merged). This should have been included in the stage0.toml in bbd3260. This has been fixed and now the dice feature is enabled by default. If DICE is disabled on an lpc55 executing this code then none of the artifacts are generated and stage0 kicks off the hubris image like it always has. |
lib/dice/src/handoff.rs
Outdated
|
||
// This memory is the USB peripheral SRAM that's 0x4000 bytes long. Changes | ||
// to this address must be coordinated with the [dice_*] tables in | ||
// chips/lpc55/chips.toml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
build.rs could generate these from the config.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spent a bit of time looking at this and while I agree in general it would require adding some plumbing to xtask. The data we would need is in the 'chip' key from the top level toml table but AFAIK the currently available methods for getting at the app toml don't expose it:
build_util::config
exposes the app config that includes tables under[config]
build_util::task_config
exposes tables under the[task_name]
table
That's not to say it can't be done, but AFAIK doing so would require a new interface in build_util and some plumbing in xtask to expose the chip key / file path. This is definitely something that should be added but maybe it can wait till after this PR is merged? #731
47998cd
to
78d82c3
Compare
@@ -46,6 +46,12 @@ size = 4096 | |||
address = 0x4003A000 | |||
size = 4096 | |||
|
|||
# this is the start of the USB SRAM AHB peripheral (0x4000 bytes total) | |||
# we appropriate this SRAM for passing DICE artifacts | |||
[dice_alias] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer that peripherals, especially those defined in the chip file, reflect the actual peripheral rather than our usage of it. In this case, renaming to usb_sram and extending to the full size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally I'd agree with you here but eventually we'll be transferring other collections of artifacts across this boundary. In a future where we're transferring credentials to multiple recipients / hubris tasks we'll want guarantees that each task is only able to access the appropriate credentials. AFAIK, by dividing the USB SRAM into regions in the chip.toml we're able to grant access to each region using the existing mechanisms in the kernel. I wrote this up in RFD 282: https://rfd.shared.oxide.computer/rfd/0282#_ram
Use of the USB SRAM was a choice of convenience and so if the issue is with this memory being associated with a peripheral we could select any sufficiently large & unused region of memory for this purpose. This would still require that we list it in the chip.toml file as separate memory regions but it would keep us from labeling the USB SRAM as anything but what it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we use SRAM 4 instead (0x20040000-0x20043FFF
)? That seems less weird than using spare USB SRAM, and isn't otherwise used in the system.
(I'm not sure if we'd need to jump through any hoops at startup to turn it on, or whether it's on immediately)
6bb2a73
to
fce77ef
Compare
&self.as_bytes()[Self::PUB_START..Self::PUB_END] | ||
} | ||
|
||
fn set_pub(mut self, pubkey: &[u8; PUBLICKEY_SERIALIZED_LENGTH]) -> Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: in cases where we have a _LENGTH
constant, could we remove the _END
and use *_START + *LENGTH
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could. This case is a bit weird since this _LENGTH
is coming from a different library. But either way _START
, _END
, _LENGTH
stuff from the *_tmpl.rs
is mostly generated at this point so the _LENGTH
values in these files are just calculated values from _START
and _END
behind the scenes.
#[allow(dead_code)] | ||
pub const SIZE: usize = 608; | ||
#[allow(dead_code)] | ||
pub const SERIAL_NUMBER_START: usize = 15; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: these could be more compactly expressed as ranges, e.g.
pub const SERIAL_NUMBER_RANGE: core::ops::Range<usize> = 15..16;
Doing so would reduce the potential for copy-paste errors!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL core::ops::Range
exists. The code that generates these tmpl files can't go into the hubris tree cleanly since I'm using a bunch of external tools to do ASN.1 / x509 crimes but I'll queue this change up for the code gen. This may also help w/ your previous point about _LENGTH
values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fn as_bytes(&self) -> &[u8]; | ||
fn as_mut_bytes(&mut self) -> &mut [u8]; | ||
|
||
const SERIAL_NUMBER_START: usize; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(same comment here about using core::ops::Range<usize>
)
const SERIAL_NUMBER_START: usize; | ||
const SERIAL_NUMBER_END: usize; | ||
|
||
fn get_serial_number(&self) -> u8 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional:
If you want to get fancy here, you could add a helper function of the form
fn read_range<'a, T>(&'a self, r: core::ops::Range<usize>) -> T
where T: TryFrom<&'a [u8]>
{
self.as_bytes()[r].try_into().unwrap_lite()
}
Then, get_serial_number
would become
fn get_serial_number(&self) -> u8 {
u8::from_be_bytes(self.read_range(Self::SERIAL_NUMBER_RANGE))
}
(and similarly, the other getters would be a little less verbose)
.set_issuer_sn(dname_sn) | ||
.set_subject_sn(dname_sn) | ||
.set_pub(keypair.public.as_bytes()) | ||
.sign(keypair) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vague architectural vibes (no need to take action): the API would be more robust if we split up the intermediate vs signed certificate, e.g. if we had CertBuilder::sign(self, keypair: &Keypair) -> Cert
This would protect us from editing the certificate after it was signed. I'm not exactly sure how this would work with the generics – maybe the Cert
type would be an associated type on CertBuilder
, and it would store the various fields?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely like the idea of minimizing potential mistakes. I'm not sure how the generics would work either though. The two cert types have different fields (only one, but still different) so we'd need a different builder for each cert type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't notice that there was a difference in the Wall Of Fields. In that case, we'd need separate builder types for each one (which might even let us kill the Cert
trait entirely?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will probably cause everything in the Cert
trait to move into another trait for the builders. Just moves things around.
lib/dice/src/cert.rs
Outdated
0x88, 0x35, 0x50, 0x0E, | ||
]; | ||
|
||
// Each time this library is built the notBefore time in the validity |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anything in this library dealing with notBefore. Is this a TODO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had initially set this up such that the cert templates were generated at build time. I pulled this out because it had weird dependencies on external tools used to create the x.509 structures. In constructing the templates I was initially setting the notBefore field with the system time before I realized that this would break reproducible builds. This can be done in such a way it won't break reproducible builds but it requires support from xtask. I'll make a ticket and add a TODO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lib/dice/src/handoff.rs
Outdated
// 1) The associated memory region has been enabled / turned on if | ||
// necessary. This happens in the constructor / 'turn_on' function. | ||
// 2) The function call is made by code in stage0. | ||
let dst: &mut [u8] = unsafe { &mut *dst_ptr }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let dst: &mut [u8] = unsafe { &mut *dst_ptr }; | |
let dst: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(ALIAS_START as *mut u8, AliasData::MAX_SIZE) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also check that AliasData::MAX_SIZE fits in the dice_alias peripheral's range. That should be a compile-time check of some sort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lib/dice/src/handoff.rs
Outdated
// conditions are met: | ||
// 1) The associated memory region has been enabled / turned on if | ||
// necessary. This happens in the constructor / 'turn_on' function. | ||
// 2) The function call is made by code in stage0. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels worth clarifying in the safety comment that this is safe even if these conditions aren't met since it will cause a fault.
lib/dice/src/handoff.rs
Outdated
// necessary. This should be done by code in stage0. | ||
// 2) The task making the call has been granted access to the memory | ||
// region by the kernel. | ||
let src: &[u8] = unsafe { &*src_ptr }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let src: &[u8] = unsafe { &*src_ptr }; | |
let src: &[u8] = unsafe { core::slice::from_raw_parts(ALIAS_START as *const u8, AliasData::MAX_SIZE) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about checking that AliasData::MAX_SIZE is less than or equal to dice_alias peripheral size.
// 1) The associated memory region has been enabled / turned on if | ||
// necessary. This should be done by code in stage0. | ||
// 2) The task making the call has been granted access to the memory | ||
// region by the kernel. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment about noting that this is safe even if these conditions are violated as it will cause a fault.
lib/dice/src/lib.rs
Outdated
if self.0.iter().all(|&w| w == 0) { | ||
true | ||
} else { | ||
false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if self.0.iter().all(|&w| w == 0) { | |
true | |
} else { | |
false | |
} | |
self.0.iter().all(|&w| w == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oof 🤣
|
||
for (dst, src) in cdi | ||
.chunks_exact_mut(mem::size_of::<u32>()) | ||
.zip(cdi_reg.0.as_ref()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl SeedBuf on CdiReg instead of cdi_reg.0.as_ref()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CdiReg is a slice of u32s not bytes like the SeedBufs. The type exists pretty much exclusively to ensure the registers that it wraps are zeroed when it's dropped.
pub fn run(image: &Image) { | ||
let cdi = match Cdi::from_reg() { | ||
Some(cdi) => cdi, | ||
None => return, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the CDI isn't available, this is going to skip turning on the USB SRAM. That means that any task that attempts to access the DICE artifacts in stage1 will fault when they try to access it. Probably better to enable the USB SRAM and write out a well-known pattern indicating that DICE was disabled in stage0 so that can reported upstream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This came up elsewhere in the review and I 💯 agree. For now the well-known pattern is 0's but I'll do some testing to see how well this works in practice.
The current cert implementation is limited to the creation of a self signed cert for the DeviceId & the Alias cert. Cert creation is as simple as possible with a template for each cert (DER) that is populated with fixed length data like the platform serial number, certificate serial number, public key & signature. Future work will address programatic generation of these templates at build time to replace the currently static ones.
The memory used in this commit is SRAM intended for use by USB. The choice of using this memory region was mostly driven by convenience and pivoting to a different range should be straight forward. The size of this region was chosen based on the estimated size of the data passed. The DeviceId and Alias cert are each ~600 bytes and the seed required to reconstruct the Alias key is 32 bytes. All of this fits comfortably in 2k.
…ials. This commit collects the DICE CDI from the NXP ROM, creates an ed25519 key pair from it (effectively the DeviceId) and uses this key to create a self-signed DeviceId cert. The hash of the hubris image that will be launched by stage0 is then hashed and the output used to generate the Alias credentials (used for attestation). Finally the keying material used to create the Alias key, and the cert chain from the Alias back to the DeviceId are written to memory for use in an attestation exchange by a future hubris task. If run on an lpc55 with DICE disabled (the default config) none of the DICE artifacts will be created and hubris will be booted as usual.
* Update lpc55_support for compatibility with latest zeroize crate. * lib-dice: Add new library with types for DICE artifacts and certs. The current cert implementation is limited to the creation of a self signed cert for the DeviceId & the Alias cert. Cert creation is as simple as possible with a template for each cert (DER) that is populated with fixed length data like the platform serial number, certificate serial number, public key & signature. Future work will address programatic generation of these templates at build time to replace the currently static ones. * lpc55/chip.toml: Add memory regions for DICE artifact handoff. The memory used in this commit is SRAM intended for use by USB. The choice of using this memory region was mostly driven by convenience and pivoting to a different range should be straight forward. The size of this region was chosen based on the estimated size of the data passed. The DeviceId and Alias cert are each ~600 bytes and the seed required to reconstruct the Alias key is 32 bytes. All of this fits comfortably in 2k. * stage0: Use the dice library to implement minimal self-signed credentials. This commit collects the DICE CDI from the NXP ROM, creates an ed25519 key pair from it (effectively the DeviceId) and uses this key to create a self-signed DeviceId cert. The hash of the hubris image that will be launched by stage0 is then hashed and the output used to generate the Alias credentials (used for attestation). Finally the keying material used to create the Alias key, and the cert chain from the Alias back to the DeviceId are written to memory for use in an attestation exchange by a future hubris task. If run on an lpc55 with DICE disabled (the default config) none of the DICE artifacts will be created and hubris will be booted as usual.
* Update lpc55_support for compatibility with latest zeroize crate. * lib-dice: Add new library with types for DICE artifacts and certs. The current cert implementation is limited to the creation of a self signed cert for the DeviceId & the Alias cert. Cert creation is as simple as possible with a template for each cert (DER) that is populated with fixed length data like the platform serial number, certificate serial number, public key & signature. Future work will address programatic generation of these templates at build time to replace the currently static ones. * lpc55/chip.toml: Add memory regions for DICE artifact handoff. The memory used in this commit is SRAM intended for use by USB. The choice of using this memory region was mostly driven by convenience and pivoting to a different range should be straight forward. The size of this region was chosen based on the estimated size of the data passed. The DeviceId and Alias cert are each ~600 bytes and the seed required to reconstruct the Alias key is 32 bytes. All of this fits comfortably in 2k. * stage0: Use the dice library to implement minimal self-signed credentials. This commit collects the DICE CDI from the NXP ROM, creates an ed25519 key pair from it (effectively the DeviceId) and uses this key to create a self-signed DeviceId cert. The hash of the hubris image that will be launched by stage0 is then hashed and the output used to generate the Alias credentials (used for attestation). Finally the keying material used to create the Alias key, and the cert chain from the Alias back to the DeviceId are written to memory for use in an attestation exchange by a future hubris task. If run on an lpc55 with DICE disabled (the default config) none of the DICE artifacts will be created and hubris will be booted as usual.
This PR adds a minimal, self-signed DeviceId and Alias cert creation process to stage0. The majority of the functionality is collected in a library that provides a types for various artifacts (Cdi, CdiL0, AliasOkm etc), functions for their derivation, and an interface to build the DeviceId and Alias certs. The artifacts created by stage0 (Alias seed, Alias cert & DeviceId cert) are then passed through RAM to a demonstration / test task in hubris. This task extracts the relevant artifacts, checks signatures on the certs and dumps the FWID out to a ringbuf.
NOTE: The demonstration task should be removed from this PR before merging as it serves no useful purpose beyond demo / debug.