Skip to content
Open
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
118 changes: 73 additions & 45 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,91 @@ on: [push, pull_request]

name: Test

permissions:
contents: read

env:
RUSTFLAGS: "-D warnings"

jobs:
no_std:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- 1.85.0 # MSRV
- stable
target:
- thumbv7em-none-eabi
- wasm32-unknown-unknown
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
targets: ${{ matrix.target }}
- run: cargo build --target ${{ matrix.target }}

test:
name: cargo test
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- 1.85.0 # MSRV
- stable
- beta
- nightly
- 1.60.0
steps:
- name: checkout
uses: actions/checkout@v2
- name: toolchain
uses: actions-rs/toolchain@v1
uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: thumbv7em-none-eabi
override: true
- name: test
uses: actions-rs/cargo@v1
with:
command: test
- name: nightly
uses: actions-rs/cargo@v1
with:
command: test
args: --features nightly
- name: no-default-features
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features
- name: std
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features std
- name: std const-generics
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features "std const-generics"
- name: std i128
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features "std i128"
- name: std i128 const-generics
uses: actions-rs/cargo@v1
- run: cargo check
- run: cargo test
- run: cargo test --release

# Test using `cargo careful`
test-careful:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- run: cargo install cargo-careful
- run: cargo careful test --all-features

# Test using `cargo miri`
test-miri:
runs-on: ubuntu-latest
env:
MIRIFLAGS: "-Zmiri-symbolic-alignment-check -Zmiri-strict-provenance"
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- s390x-unknown-linux-gnu
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@nightly
- run: rustup component add miri && cargo miri setup
- run: cargo miri test --target ${{ matrix.target }} --all-features --lib

clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
command: test
args: --no-default-features --features "std i128 const-generics"
- name: no std build
uses: actions-rs/cargo@v1
toolchain: 1.92.0 # Pinned to prevent breakages
components: clippy
- run: cargo clippy --workspace --all-features --lib --bins --tests -- -D warnings

rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
command: build
args: --no-default-features --target thumbv7em-none-eabi
toolchain: stable
components: rustfmt
- run: cargo fmt --all -- --check
26 changes: 5 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
name = "subtle"
# Before incrementing:
# - update CHANGELOG
# - update html_root_url
# - update README if necessary by semver
# - if any updates were made to the README, also update the module documentation in src/lib.rs
version = "2.6.0"
edition = "2018"
version = "3.0.0-pre"
edition = "2024"
rust-version = "1.85.0"
authors = ["Isis Lovecruft <isis@patternsinthevoid.net>",
"Henry de Valence <hdevalence@hdevalence.ca>"]
readme = "README.md"
Expand All @@ -17,23 +17,7 @@ documentation = "https://docs.rs/subtle"
categories = ["cryptography", "no-std"]
keywords = ["cryptography", "crypto", "constant-time", "utilities"]
description = "Pure-Rust traits and utilities for constant-time cryptographic implementations."
exclude = [
"**/.gitignore",
".travis.yml",
]

[badges]
travis-ci = { repository = "dalek-cryptography/subtle", branch = "master"}
exclude = [".github", ".gitignore"]

[dev-dependencies]
rand = { version = "0.8" }

[features]
const-generics = []
# DEPRECATED: As of 2.5.1, this feature does nothing.
core_hint_black_box = []
default = ["std", "i128"]
std = []
i128 = []
# DEPRECATED: As of 2.4.1, this feature does nothing.
nightly = []
rand = { version = "0.9" }
171 changes: 171 additions & 0 deletions src/choice.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use crate::{ConditionallySelectable, ConstantTimeEq};
use core::hint::black_box;
use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};

/// The `Choice` struct represents a choice for use in conditional assignment.
///
/// It is a wrapper around a `u8`, which should have the value either `1` (true)
/// or `0` (false).
///
/// The conversion from `u8` to `Choice` passes the value through an optimization
/// barrier, as a best-effort attempt to prevent the compiler from inferring that
/// the `Choice` value is a boolean. This strategy is based on Tim Maclean's
/// [work on `rust-timing-shield`][rust-timing-shield], which attempts to provide
/// a more comprehensive approach for preventing software side-channels in Rust
/// code.
///
/// The `Choice` struct implements operators for AND, OR, XOR, and NOT, to allow
/// combining `Choice` values. These operations do not short-circuit.
///
/// [rust-timing-shield]:
/// https://www.chosenplaintext.ca/open-source/rust-timing-shield/security
#[derive(Copy, Clone, Debug)]
pub struct Choice(pub(crate) u8);

impl Choice {
/// Unwrap the `Choice` wrapper to reveal the underlying `u8`.
///
/// # Note
///
/// This function only exists as an **escape hatch** for the rare case
/// where it's not possible to use one of the `subtle`-provided
/// trait impls.
///
/// **To convert a `Choice` to a `bool`, use the `From` implementation instead.**
#[inline]
pub fn unwrap_u8(&self) -> u8 {
self.0
}
}

impl From<Choice> for bool {
/// Convert the `Choice` wrapper into a `bool`, depending on whether
/// the underlying `u8` was a `0` or a `1`.
///
/// # Note
///
/// This function exists to avoid having higher-level cryptographic protocol
/// implementations duplicating this pattern.
///
/// The intended use case for this conversion is at the _end_ of a
/// higher-level primitive implementation: for example, in checking a keyed
/// MAC, where the verification should happen in constant-time (and thus use
/// a `Choice`) but it is safe to return a `bool` at the end of the
/// verification.
#[inline]
fn from(source: Choice) -> bool {
debug_assert!((source.0 == 0u8) | (source.0 == 1u8));
source.0 != 0
}
}

impl BitAnd for Choice {
type Output = Choice;
#[inline]
fn bitand(self, rhs: Choice) -> Choice {
(self.0 & rhs.0).into()
}
}

impl BitAndAssign for Choice {
#[inline]
fn bitand_assign(&mut self, rhs: Choice) {
*self = *self & rhs;
}
}

impl BitOr for Choice {
type Output = Choice;
#[inline]
fn bitor(self, rhs: Choice) -> Choice {
(self.0 | rhs.0).into()
}
}

impl BitOrAssign for Choice {
#[inline]
fn bitor_assign(&mut self, rhs: Choice) {
*self = *self | rhs;
}
}

impl BitXor for Choice {
type Output = Choice;
#[inline]
fn bitxor(self, rhs: Choice) -> Choice {
(self.0 ^ rhs.0).into()
}
}

impl BitXorAssign for Choice {
#[inline]
fn bitxor_assign(&mut self, rhs: Choice) {
*self = *self ^ rhs;
}
}

impl Not for Choice {
type Output = Choice;
#[inline]
fn not(self) -> Choice {
(1u8 & (!self.0)).into()
}
}

impl From<u8> for Choice {
#[inline]
fn from(input: u8) -> Choice {
debug_assert!((input == 0u8) | (input == 1u8));

// Our goal is to prevent the compiler from inferring that the value held inside the
// resulting `Choice` struct is really a `bool` instead of a `u8`.
Choice(black_box(input))
}
}

impl ConditionallySelectable for Choice {
#[inline]
fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self {
Choice(u8::conditional_select(&a.0, &b.0, choice))
}
}

impl ConstantTimeEq for Choice {
#[inline]
fn ct_eq(&self, rhs: &Choice) -> Choice {
!(*self ^ *rhs)
}
}

#[cfg(test)]
mod tests {
use crate::{Choice, ConditionallySelectable, ConstantTimeEq};

#[test]
fn choice_into_bool() {
let choice_true: bool = Choice::from(1).into();
assert!(choice_true);

let choice_false: bool = Choice::from(0).into();
assert!(!choice_false);
}

#[test]
fn conditional_select_choice() {
let t = Choice::from(1);
let f = Choice::from(0);

assert!(bool::from(Choice::conditional_select(&t, &f, f)));
assert!(!bool::from(Choice::conditional_select(&t, &f, t)));
assert!(!bool::from(Choice::conditional_select(&f, &t, f)));
assert!(bool::from(Choice::conditional_select(&f, &t, t)));
}

#[test]
fn choice_equal() {
assert_eq!(Choice::from(0).ct_eq(&Choice::from(0)).unwrap_u8(), 1);
assert_eq!(Choice::from(0).ct_eq(&Choice::from(1)).unwrap_u8(), 0);
assert_eq!(Choice::from(1).ct_eq(&Choice::from(0)).unwrap_u8(), 0);
assert_eq!(Choice::from(1).ct_eq(&Choice::from(1)).unwrap_u8(), 1);
}
}
Loading