Skip to content
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

Long-term secret erasure #9

Merged
merged 2 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: ubuntu-latest
- os: macos-latest
- os: windows-latest
- os: ubuntu-latest
cross: aarch64-unknown-linux-gnu

runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -29,6 +35,12 @@ jobs:
if: runner.os == 'Windows'
uses: ilammy/setup-nasm@v1

- name: Install cross
if: matrix.cross != ''
uses: taiki-e/setup-cross-toolchain-action@v1
with:
target: ${{ matrix.cross }}

- name: Build (debug)
run: cargo build -p graviola
- name: Run tests (debug)
Expand All @@ -40,7 +52,7 @@ jobs:
run: cargo test --release

- name: Artificial CPU feature tests (x86_64)
if: runner.os == 'Linux'
if: runner.arch == 'X64'
run: |
# test software fallbacks for sha256 and sha512
env GRAVIOLA_CPU_DISABLE_sha=1 GRAVIOLA_CPU_DISABLE_bmi2=1 cargo test
4 changes: 4 additions & 0 deletions graviola/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ serde_json = "1"
[[test]]
name = "wycheproof"
required-features = ["__internal_08eaf2eb"]

[[test]]
name = "zeroing"
required-features = ["__internal_08eaf2eb"]
7 changes: 4 additions & 3 deletions graviola/src/high/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::hash::{Hash, HashContext};
use super::hmac_drbg::HmacDrbg;
use super::pkcs8;
use crate::error::{Error, KeyFormatError};
use crate::low::Entry;
use crate::low::{zeroise, Entry};
use crate::mid::rng::{RandomSource, SystemRandom};

pub struct SigningKey<C: Curve> {
Expand Down Expand Up @@ -121,8 +121,8 @@ impl<C: Curve> SigningKey<C> {
}
let hash = ctx.finish();

let mut encoded_private_key = [0u8; MAX_SCALAR_LEN];
let encoded_private_key = self.private_key.encode(&mut encoded_private_key)?;
let mut encoded_private_key_buf = [0u8; MAX_SCALAR_LEN];
let encoded_private_key = self.private_key.encode(&mut encoded_private_key_buf)?;

let e = hash_to_scalar::<C>(hash.as_ref())?;
let mut e_bytes = [0u8; MAX_SCALAR_LEN];
Expand All @@ -132,6 +132,7 @@ impl<C: Curve> SigningKey<C> {
&e_bytes[..C::Scalar::LEN_BYTES],
random,
);
zeroise(&mut encoded_private_key_buf);

let (k, r) = loop {
let k = C::generate_random_key(&mut rng)?;
Expand Down
10 changes: 5 additions & 5 deletions graviola/src/high/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ impl RsaPrivateSigningKey {
.map_err(Error::Asn1Error)?;
let e = e.try_into().map_err(|_| Error::OutOfRange)?;

let p = PosInt::from_bytes(decoded.prime1.as_ref())?;
let q = PosInt::from_bytes(decoded.prime2.as_ref())?;
let dp = PosInt::from_bytes(decoded.exponent1.as_ref())?;
let dq = PosInt::from_bytes(decoded.exponent2.as_ref())?;
let iqmp = PosInt::from_bytes(decoded.coefficient.as_ref())?;
let p = PosInt::from_bytes(decoded.prime1.as_ref())?.into();
let q = PosInt::from_bytes(decoded.prime2.as_ref())?.into();
let dp = PosInt::from_bytes(decoded.exponent1.as_ref())?.into();
let dq = PosInt::from_bytes(decoded.exponent2.as_ref())?.into();
let iqmp = PosInt::from_bytes(decoded.coefficient.as_ref())?.into();

let priv_key = rsa_priv::RsaPrivateKey::new(p, q, dp, dq, iqmp, n, e)?;
Ok(Self(priv_key))
Expand Down
13 changes: 13 additions & 0 deletions graviola/src/low/aarch64/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//
// cf. the x86_64 version, on which this one is based.

use crate::low;
use core::arch::aarch64::*;

pub(crate) enum AesKey {
Expand Down Expand Up @@ -88,6 +89,12 @@ impl AesKey128 {
}
}

impl Drop for AesKey128 {
fn drop(&mut self) {
low::zeroise(&mut self.round_keys);
}
}

pub(crate) struct AesKey256 {
round_keys: [uint8x16_t; 14 + 1],
}
Expand Down Expand Up @@ -131,6 +138,12 @@ impl AesKey256 {
}
}

impl Drop for AesKey256 {
fn drop(&mut self) {
low::zeroise(&mut self.round_keys);
}
}

fn zero() -> uint8x16_t {
unsafe { vdupq_n_u8(0) }
}
Expand Down
28 changes: 28 additions & 0 deletions graviola/src/low/aarch64/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@ pub(crate) fn leave_cpu_state(old: u32) {
dit::maybe_disable(old);
}

/// Effectively memset(ptr, 0, len), but not visible to optimiser
pub(crate) fn zero_bytes(ptr: *mut u8, len: usize) {
unsafe {
core::arch::asm!(
" eor {zero}.16b, {zero}.16b, {zero}.16b",
// by-16 loop
" 2: cmp {len}, #16",
" blt 3f",
" st1 {{{zero}.16b}}, [{ptr}]",
" add {ptr}, {ptr}, #16",
" sub {len}, {len}, #16",
" b 2b",
// by-1 loop
" 3: subs {len}, {len}, #1",
" blt 4f",
" strb wzr, [{ptr}], #1",
" b 3b",
" 4: ",

ptr = inout(reg) ptr => _,
len = inout(reg) len => _,

// clobbers
zero = out(vreg) _,
)
}
}

pub(crate) fn verify_cpu_features() {
assert!(
is_aarch64_feature_detected!("neon"),
Expand Down
8 changes: 8 additions & 0 deletions graviola/src/low/aarch64/ghash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//!
//! Based on the implementation in low/x86_64/ghash.rs

use crate::low;
use core::arch::aarch64::*;
use core::mem;

Expand Down Expand Up @@ -42,6 +43,13 @@ impl GhashTable {
}
}

impl Drop for GhashTable {
fn drop(&mut self) {
low::zeroise(&mut self.powers);
low::zeroise(&mut self.powers_xor);
}
}

pub(crate) struct Ghash<'a> {
table: &'a GhashTable,
current: uint64x2_t,
Expand Down
31 changes: 31 additions & 0 deletions graviola/src/low/generic/zeroise.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Written for Graviola by Joe Birr-Pixton, 2024.
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0

use crate::low::zero_bytes;

/// Writes zeroes over the whole of the `v` slice.
pub(crate) fn zeroise<T: Zeroable>(v: &mut [T]) {
zero_bytes(v.as_mut_ptr().cast(), size_of_val(v));
}

/// Writes zeroes over the whole of the `v` value.
pub(crate) fn zeroise_value<T: Zeroable>(v: &mut T) {
zero_bytes(v as *mut T as *mut _, size_of::<T>());
}

/// Marker trait for types who have valid all-bits-zero values.
pub(crate) trait Zeroable {}

impl Zeroable for u8 {}
impl Zeroable for u64 {}
impl Zeroable for usize {}

#[cfg(target_arch = "x86_64")]
impl Zeroable for core::arch::x86_64::__m256i {}
#[cfg(target_arch = "x86_64")]
impl Zeroable for core::arch::x86_64::__m128i {}

#[cfg(target_arch = "aarch64")]
impl Zeroable for core::arch::aarch64::uint8x16_t {}
#[cfg(target_arch = "aarch64")]
impl Zeroable for core::arch::aarch64::uint64x2_t {}
8 changes: 5 additions & 3 deletions graviola/src/low/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod generic {
pub(crate) mod poly1305;
pub(super) mod sha256;
pub(super) mod sha512;
pub(super) mod zeroise;
}

mod entry;
Expand All @@ -27,7 +28,8 @@ pub(crate) use entry::Entry;
pub(crate) use generic::blockwise::Blockwise;
pub(crate) use generic::ct_equal::ct_equal;
pub(crate) use generic::poly1305;
pub(crate) use posint::PosInt;
pub(crate) use generic::zeroise::{zeroise, zeroise_value};
pub(crate) use posint::{PosInt, SecretPosInt};

#[cfg(test)]
mod tests;
Expand All @@ -36,7 +38,7 @@ cfg_if::cfg_if! {
if #[cfg(target_arch = "x86_64")] {
mod x86_64;

pub(crate) use x86_64::cpu::{enter_cpu_state, leave_cpu_state, verify_cpu_features};
pub(crate) use x86_64::cpu::{enter_cpu_state, zero_bytes, leave_cpu_state, verify_cpu_features};
pub(crate) use x86_64::chacha20;
pub(crate) use x86_64::aes::AesKey;
pub(crate) use x86_64::aes_gcm;
Expand Down Expand Up @@ -95,7 +97,7 @@ cfg_if::cfg_if! {
} else if #[cfg(target_arch = "aarch64")] {
mod aarch64;

pub(crate) use aarch64::cpu::{enter_cpu_state, leave_cpu_state, verify_cpu_features};
pub(crate) use aarch64::cpu::{enter_cpu_state, zero_bytes, leave_cpu_state, verify_cpu_features};
pub(crate) use aarch64::aes::AesKey;
pub(crate) use aarch64::bignum_add::bignum_add;
pub(crate) use aarch64::bignum_add_p256::bignum_add_p256;
Expand Down
35 changes: 35 additions & 0 deletions graviola/src/low/posint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
use crate::low;
use crate::Error;

use core::ops::{Deref, DerefMut};

#[derive(Clone, Debug)]
pub(crate) struct PosInt<const N: usize> {
words: [u64; N],
Expand Down Expand Up @@ -452,6 +454,7 @@ impl<const N: usize> PosInt<N> {
}
}

low::zeroise(&mut table);
accum.from_montgomery(n)
}

Expand Down Expand Up @@ -489,6 +492,38 @@ impl<const N: usize> PosInt<N> {
}
}

/// A `SecretPosInt` is a `PosInt` containing long-term key material.
///
/// It is zeroed on drop.
pub(crate) struct SecretPosInt<const N: usize>(PosInt<N>);

impl<const N: usize> From<PosInt<N>> for SecretPosInt<N> {
fn from(pi: PosInt<N>) -> Self {
Self(pi)
}
}

impl<const N: usize> Deref for SecretPosInt<N> {
type Target = PosInt<N>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<const N: usize> DerefMut for SecretPosInt<N> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl<const N: usize> Drop for SecretPosInt<N> {
fn drop(&mut self) {
low::zeroise(self.as_mut_words());
low::zeroise_value(&mut self.used);
}
}

#[derive(Debug)]
struct BitsMsbFirstIter<'a> {
words: &'a [u64],
Expand Down
14 changes: 14 additions & 0 deletions graviola/src/low/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ fn bignum_mux() {
bignum_mux_equiv(u64::MAX, &[1; 1], &[0; 1]);
}

#[test]
fn zeroise() {
for n in 0..1024 {
zeroise_equiv(n);
}
}

fn zeroise_equiv(len: usize) {
let expect = vec![0x00u8; len];
let mut bytes = vec![0xffu8; len];
super::zeroise(&mut bytes);
assert_eq!(expect, bytes);
}

mod model {
pub fn bignum_mux(p: u64, z: &mut [u64], x_if_p: &[u64], y_if_not_p: &[u64]) {
if p > 0 {
Expand Down
14 changes: 14 additions & 0 deletions graviola/src/low/x86_64/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

use core::arch::x86_64::*;

use crate::low;

pub(crate) enum AesKey {
Aes128(AesKey128),
Aes256(AesKey256),
Expand Down Expand Up @@ -70,6 +72,12 @@ impl AesKey128 {
}
}

impl Drop for AesKey128 {
fn drop(&mut self) {
low::zeroise(&mut self.round_keys);
}
}

fn zero() -> __m128i {
unsafe { _mm_setzero_si128() }
}
Expand All @@ -94,6 +102,12 @@ impl AesKey256 {
}
}

impl Drop for AesKey256 {
fn drop(&mut self) {
low::zeroise(&mut self.round_keys);
}
}

macro_rules! expand_128 {
($rcon:literal, $t1:ident, $out:expr) => {
// with [X3, _, X1, _] = t1
Expand Down
32 changes: 32 additions & 0 deletions graviola/src/low/x86_64/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ pub(crate) fn leave_cpu_state(_old: u32) {
}
}

/// Effectively memset(ptr, 0, len), but not visible to optimiser
pub(crate) fn zero_bytes(ptr: *mut u8, len: usize) {
unsafe { _zero_bytes(ptr, len) }
}

#[target_feature(enable = "avx")]
unsafe fn _zero_bytes(ptr: *mut u8, len: usize) {
core::arch::asm!(
" vpxor {zero}, {zero}, {zero}",
// by-32 loop
" 2: cmp {len}, 32",
" jl 3f",
" vmovdqu [{ptr}], {zero}",
" add {ptr}, 32",
" sub {len}, 32",
" jmp 2b",
// by-1 loop
" 3: sub {len}, 1",
" jl 4f",
" mov byte ptr [{ptr}], 0",
" add {ptr}, 1",
" jmp 3b",
" 4: ",

ptr = inout(reg) ptr => _,
len = inout(reg) len => _,

// clobbers
zero = out(ymm_reg) _,
)
}

/// This macro interdicts is_x86_feature_detected to
/// allow testability.
macro_rules! have_cpu_feature {
Expand Down
Loading