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

Verify CPU features, run code at library entry/exit #5

Merged
merged 5 commits into from
Sep 30, 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
10 changes: 8 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ jobs:
- name: Build (debug)
run: cargo build -p graviola
- name: Run tests (debug)
run: cargo test -p graviola
run: cargo test

- name: Build (release)
run: cargo build -p graviola --release
- name: Run tests (release)
run: cargo test -p graviola --release
run: cargo test --release

- name: Artificial CPU feature tests (x86_64)
if: runner.os == 'Linux'
run: |
# test software fallbacks for sha256 and sha512
env GRAVIOLA_CPU_DISABLE_sha=1 GRAVIOLA_CPU_DISABLE_bmi2=1 cargo test
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,16 @@ are not welcomed; please do not file issues or PRs.
- [x] Competitive performance (with *ring*, aws-lc-rs, and rustcrypto)
- [x] Uses formally-verified assembler from other projects (where available)
- [x] Intended to provide algorithms in wide use on web
- [x] Intended for use as a rustls `CryptoProvider`
- [x] Intended for use as a rustls `CryptoProvider`, via [rustls-graviola][].

## Limitations

`aarch64` and `x86_64` architectures only.

- `aarch64` requires `aes`, `sha2`, `pmull`, and `neon` CPU features.
(This notably excludes Raspberry PI 4 and earlier, but covers Raspberry Pi 5.)
- `x86_64` requires `aes`, `ssse3` `avx`, `avx2`, `bmi2`, and `pclmulqdq` CPU features.
(This is most x86_64 CPUs made since around 2013.)

## Acknowledgements and Thanks

Expand All @@ -52,6 +54,7 @@ We are grateful to:
[wycheproof]: https://github.com/C2SP/wycheproof
[SLOTHY]: https://github.com/slothy-optimizer/slothy
[performance]: https://jbp.io/graviola/
[rustls-graviola]: https://crates.io/crates/rustls-graviola

## Algorithms

Expand Down Expand Up @@ -119,8 +122,8 @@ X25519 directly uses the s2n-bignum implementation.

### Symmetric cryptography
SHA256 has straightforward implementations using hashing intrinsics
(aka "SHA-NI" on x86_64, "sha" extension on aarch64) with runtime fallback to
a pure Rust version if needed.
(aka "SHA-NI" on x86_64, "sha" extension on aarch64) with runtime fallback
on x86_64 to a pure Rust version if needed.

SHA384/SHA512 on x86_64 has an AVX2 by-4 implementation.

Expand All @@ -136,9 +139,9 @@ kept spilling registers and was slower.)

We have broadly three module layers:

- `low`: low level primitives. private. platform-specific. unsafe allowed. `no_std`. no alloc.
- `mid`: constructions, protocols and encodings. private. platform agnostic. no unsafe. `no_std`. alloc.
- `high`: public interface, primarily a rustls `CryptoProvider`. platform agnostic. no unsafe.
- `low`: low level primitives. private. platform-specific. unsafe allowed. minimal std and alloc.
- `mid`: constructions, protocols and encodings. private. platform agnostic. no unsafe. minimal std and alloc.
- `high`: high level encodings and operations. public. platform agnostic. no unsafe.

`low` code should not refer to `mid`, nor `mid` to `high`.

Expand Down
8 changes: 8 additions & 0 deletions graviola/src/high/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +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::mid::rng::{RandomSource, SystemRandom};

pub struct SigningKey<C: Curve> {
Expand All @@ -16,6 +17,7 @@ pub struct SigningKey<C: Curve> {
impl<C: Curve> SigningKey<C> {
/// Load an ECDSA private key in PKCS#8 format.
pub fn from_pkcs8_der(bytes: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_secret();
pkcs8::decode_pkcs8(
bytes,
&asn1::oid::id_ecPublicKey,
Expand All @@ -26,6 +28,7 @@ impl<C: Curve> SigningKey<C> {

/// Load an ECDSA private key in SEC.1 format.
pub fn from_sec1_der(bytes: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_secret();
let ecpk = asn1::pkix::EcPrivateKey::from_bytes(bytes).map_err(Error::Asn1Error)?;

if !matches!(ecpk.version, asn1::pkix::EcPrivateKeyVer::ecPrivkeyVer1) {
Expand All @@ -50,6 +53,7 @@ impl<C: Curve> SigningKey<C> {
message: &[&[u8]],
signature: &'a mut [u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
let mut random = [0u8; 16];
SystemRandom.fill(&mut random)?;
self.rfc6979_sign_with_random::<H>(message, &random, signature)
Expand All @@ -64,6 +68,7 @@ impl<C: Curve> SigningKey<C> {
message: &[&[u8]],
asn1_signature: &'a mut [u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
let mut fixed_sig = [0u8; MAX_SCALAR_LEN * 2];
let fixed_sig = self.sign::<H>(message, &mut fixed_sig)?;

Expand Down Expand Up @@ -150,6 +155,7 @@ pub struct VerifyingKey<C: Curve> {
impl<C: Curve> VerifyingKey<C> {
/// Create a `VerifyingKey` by decoding an X9.62 uncompressed point.
pub fn from_x962_uncompressed(encoded: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_public();
C::PublicKey::from_x962_uncompressed(encoded).map(|public_key| Self { public_key })
}

Expand All @@ -162,6 +168,7 @@ impl<C: Curve> VerifyingKey<C> {
///
/// Returns nothing when the signature is valid, or `Error::BadSignature` if not.
pub fn verify<H: Hash>(&self, message: &[&[u8]], signature: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
if signature.len() != C::Scalar::LEN_BYTES * 2 {
return Err(Error::WrongLength);
}
Expand Down Expand Up @@ -191,6 +198,7 @@ impl<C: Curve> VerifyingKey<C> {
/// This does a straightforward conversion from ASN.1 to fixed length,
/// and then calls [`Self::verify()`] -- see the documentation for more.
pub fn verify_asn1<H: Hash>(&self, message: &[&[u8]], signature: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
let sig =
asn1::pkix::EcdsaSigValue::from_bytes(signature).map_err(|_| Error::BadSignature)?;
if sig.r.is_negative() || sig.s.is_negative() {
Expand Down
2 changes: 1 addition & 1 deletion graviola/src/high/hmac_drbg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct HmacDrbg<H: Hash> {
}

impl<H: Hash> HmacDrbg<H> {
pub fn new(entropy_input: &[u8], nonce: &[u8], personalization_string: &[u8]) -> Self {
pub(crate) fn new(entropy_input: &[u8], nonce: &[u8], personalization_string: &[u8]) -> Self {
// 1. seed_material = entropy_input || nonce || personalization_string.
let seed_material = &[entropy_input, nonce, personalization_string];

Expand Down
18 changes: 18 additions & 0 deletions graviola/src/high/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use crate::high::asn1::{self, pkix, Type};
use crate::high::hash::{self, Hash};
use crate::high::{pkcs1, pkcs8};
use crate::low::Entry;
use crate::low::PosInt;
use crate::mid::rng::SystemRandom;
use crate::mid::{rsa_priv, rsa_pub};
Expand All @@ -14,6 +15,7 @@ pub struct RsaPublicVerificationKey(rsa_pub::RsaPublicKey);

impl RsaPublicVerificationKey {
pub fn from_pkcs1_der(bytes: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_public();
let decoded = pkix::RSAPublicKey::from_bytes(bytes).map_err(Error::Asn1Error)?;

if decoded.modulus.is_negative() {
Expand All @@ -33,16 +35,19 @@ impl RsaPublicVerificationKey {
}

pub fn verify_pkcs1_sha256(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
let hash = hash::Sha256::hash(message);
self._verify_pkcs1(signature, pkcs1::DIGESTINFO_SHA256, hash.as_ref())
}

pub fn verify_pkcs1_sha384(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
let hash = hash::Sha384::hash(message);
self._verify_pkcs1(signature, pkcs1::DIGESTINFO_SHA384, hash.as_ref())
}

pub fn verify_pkcs1_sha512(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
let hash = hash::Sha512::hash(message);
self._verify_pkcs1(signature, pkcs1::DIGESTINFO_SHA512, hash.as_ref())
}
Expand Down Expand Up @@ -70,14 +75,17 @@ impl RsaPublicVerificationKey {
}

pub fn verify_pss_sha256(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
self._verify_pss::<hash::Sha256>(signature, message)
}

pub fn verify_pss_sha384(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
self._verify_pss::<hash::Sha384>(signature, message)
}

pub fn verify_pss_sha512(&self, signature: &[u8], message: &[u8]) -> Result<(), Error> {
let _ = Entry::new_public();
self._verify_pss::<hash::Sha512>(signature, message)
}

Expand All @@ -101,6 +109,7 @@ pub struct RsaPrivateSigningKey(rsa_priv::RsaPrivateKey);

impl RsaPrivateSigningKey {
pub fn from_pkcs1_der(bytes: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_secret();
let decoded = pkix::RSAPrivateKey::from_bytes(bytes).map_err(Error::Asn1Error)?;

if !matches!(decoded.version, pkix::Version::two_prime) {
Expand Down Expand Up @@ -129,6 +138,7 @@ impl RsaPrivateSigningKey {
}

pub fn from_pkcs8_der(bytes: &[u8]) -> Result<Self, Error> {
let _ = Entry::new_secret();
pkcs8::decode_pkcs8(
bytes,
&asn1::oid::rsaEncryption,
Expand All @@ -138,10 +148,12 @@ impl RsaPrivateSigningKey {
}

pub fn public_key(&self) -> RsaPublicVerificationKey {
let _ = Entry::new_public();
RsaPublicVerificationKey(self.0.public_key())
}

pub fn modulus_len_bytes(&self) -> usize {
let _ = Entry::new_public();
self.0.public_key().modulus_len_bytes()
}

Expand All @@ -150,6 +162,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
let hash = hash::Sha256::hash(message);
self._sign_pkcs1(signature, pkcs1::DIGESTINFO_SHA256, hash.as_ref())
}
Expand All @@ -159,6 +172,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
let hash = hash::Sha384::hash(message);
self._sign_pkcs1(signature, pkcs1::DIGESTINFO_SHA384, hash.as_ref())
}
Expand All @@ -168,6 +182,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
let hash = hash::Sha512::hash(message);
self._sign_pkcs1(signature, pkcs1::DIGESTINFO_SHA512, hash.as_ref())
}
Expand All @@ -177,6 +192,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
self._sign_pss::<hash::Sha256>(signature, message)
}

Expand All @@ -185,6 +201,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
self._sign_pss::<hash::Sha384>(signature, message)
}

Expand All @@ -193,6 +210,7 @@ impl RsaPrivateSigningKey {
signature: &'a mut [u8],
message: &[u8],
) -> Result<&'a [u8], Error> {
let _ = Entry::new_secret();
self._sign_pss::<hash::Sha512>(signature, message)
}

Expand Down
80 changes: 80 additions & 0 deletions graviola/src/low/aarch64/cpu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Written for Graviola by Joe Birr-Pixton, 2024.
// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0

use std::arch::is_aarch64_feature_detected;

pub(crate) fn enter_cpu_state() -> u32 {
dit::maybe_enable()
}

pub(crate) fn leave_cpu_state(old: u32) {
dit::maybe_disable(old);
}

pub(crate) fn verify_cpu_features() {
assert!(
is_aarch64_feature_detected!("neon"),
"graviola requires neon CPU support"
);
assert!(
is_aarch64_feature_detected!("aes"),
"graviola requires aes CPU support"
);
assert!(
is_aarch64_feature_detected!("pmull"),
"graviola requires pmull CPU support"
);
assert!(
is_aarch64_feature_detected!("sha2"),
"graviola requires sha2 CPU support"
);
}

mod dit {
pub(super) fn maybe_enable() -> u32 {
if super::is_aarch64_feature_detected!("dit") {
match unsafe { read() } {
0 => {
unsafe {
write(1);
};
1
}
_ => 0,
}
} else {
0
}
}

pub(super) fn maybe_disable(we_enabled: u32) {
if we_enabled > 0 {
unsafe { write(0) }
}
}

#[target_feature(enable = "dit")]
unsafe fn read() -> u32 {
let mut out: u64;
unsafe {
core::arch::asm!(
"mrs {r}, DIT",
r = out(reg) out,
)
};

const DIT: u64 = 0x01000000;
(out & DIT == DIT) as u32
}

#[target_feature(enable = "dit")]
unsafe fn write(on: u32) {
unsafe {
if on > 0 {
core::arch::asm!("msr DIT, #1");
} else {
core::arch::asm!("msr DIT, #0")
}
}
}
}
1 change: 1 addition & 0 deletions graviola/src/low/aarch64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub(crate) mod bignum_point_select_p256;
pub(crate) mod bignum_point_select_p384;
pub(crate) mod bignum_tomont_p256;
pub(crate) mod bignum_tomont_p384;
pub(crate) mod cpu;
pub(crate) mod curve25519_x25519;
pub(crate) mod curve25519_x25519base;
pub(crate) mod ghash;
Expand Down
Loading