Skip to content
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
126 changes: 78 additions & 48 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,36 +65,70 @@ struct Bindings {
// bindgen needs access to libclang.
// On windows, this doesn't just work, you have to set LIBCLANG_PATH.
// Rather than download the 400Mb+ files, like gecko does, let's just reuse their work.
// On macOS, clang-sys prefers the highest-versioned libclang it can find, which may be a
// Homebrew LLVM that doesn't have the correct macOS SDK include paths, resulting in broken
// bindings. Force use of Xcode's libclang instead.
fn setup_clang() {
// If this isn't Windows, or we're in CI, then we don't need to do anything.
if env::consts::OS != "windows" || env::var("GITHUB_WORKFLOW").unwrap_or_default() == "CI" {
println!("cargo:rerun-if-env-changed=LIBCLANG_PATH");
println!("cargo:rerun-if-env-changed=CI");
// In CI, the environment is already configured correctly.
if env::var("CI").is_ok() {
return;
}
println!("rerun-if-env-changed=LIBCLANG_PATH");
println!("rerun-if-env-changed=MOZBUILD_STATE_PATH");
if env::var("LIBCLANG_PATH").is_ok() {
return;
}
let mozbuild_root = if let Ok(dir) = env::var("MOZBUILD_STATE_PATH") {
PathBuf::from(dir.trim())
} else {
eprintln!("warning: Building without a gecko setup is not likely to work.");
eprintln!(" A working libclang is needed to build nss-rs.");
eprintln!(" Either LIBCLANG_PATH or MOZBUILD_STATE_PATH needs to be set.");
eprintln!();
eprintln!(" We recommend checking out https://github.com/mozilla/gecko-dev");
eprintln!(" Then run `./mach bootstrap` which will retrieve clang.");
eprintln!(" Make sure to export MOZBUILD_STATE_PATH when building.");
return;
};
let libclang_dir = mozbuild_root.join("clang").join("lib");
if libclang_dir.is_dir() {
unsafe {
env::set_var("LIBCLANG_PATH", libclang_dir.to_str().unwrap());
if env::consts::OS == "macos" {
Comment thread
larseggert marked this conversation as resolved.
if let Ok(output) = Command::new("xcode-select").arg("--print-path").output() {
if output.status.success() {
let xcode_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
let candidates = [
PathBuf::from(&xcode_path).join("Toolchains/XcodeDefault.xctoolchain/usr/lib"),
PathBuf::from(&xcode_path).join("usr/lib"),
];
if let Some(libclang_dir) = candidates.iter().find(|p| p.is_dir()) {
unsafe {
env::set_var("LIBCLANG_PATH", libclang_dir.to_str().unwrap());
}
} else {
println!(
"cargo:warning=Xcode toolchain libclang not found at {}; set LIBCLANG_PATH if build fails",
candidates[0].display()
);
}
} else {
println!(
"cargo:warning=xcode-select returned an error; set LIBCLANG_PATH if build fails"
);
}
} else {
println!("cargo:warning=xcode-select not found; set LIBCLANG_PATH if build fails");
}
} else if env::consts::OS == "windows" {
println!("cargo:rerun-if-env-changed=MOZBUILD_STATE_PATH");
let mozbuild_root = if let Ok(dir) = env::var("MOZBUILD_STATE_PATH") {
PathBuf::from(dir.trim())
} else {
println!("cargo:warning=Building without a gecko setup is not likely to work.");
println!("cargo:warning=A working libclang is needed to build nss-rs.");
println!("cargo:warning=Either LIBCLANG_PATH or MOZBUILD_STATE_PATH needs to be set.");
println!(
"cargo:warning=We recommend checking out https://github.com/mozilla/gecko-dev"
);
println!("cargo:warning=Then run `./mach bootstrap` which will retrieve clang.");
println!("cargo:warning=Make sure to export MOZBUILD_STATE_PATH when building.");
return;
};
let libclang_dir = mozbuild_root.join("clang").join("lib");
if libclang_dir.is_dir() {
unsafe {
env::set_var("LIBCLANG_PATH", libclang_dir.to_str().unwrap());
}
} else {
println!(
"cargo:warning=LIBCLANG_PATH isn't set; maybe run ./mach bootstrap with gecko"
);
}
println!("rustc-env:LIBCLANG_PATH={}", libclang_dir.to_str().unwrap());
} else {
println!("warning: LIBCLANG_PATH isn't set; maybe run ./mach bootstrap with gecko");
}
}

Expand Down Expand Up @@ -167,8 +201,7 @@ fn build_nss(dir: PathBuf) {
// Generate static libraries in addition to shared libraries.
String::from("--static"),
];
let target = env::var("TARGET").unwrap();
if target.starts_with("aarch64-") {
if env::var("CARGO_CFG_TARGET_ARCH").unwrap() == "aarch64" {
build_nss.push(String::from("--target=arm64"));
}
let status = Command::new(get_bash())
Expand All @@ -180,7 +213,8 @@ fn build_nss(dir: PathBuf) {
}

fn dynamic_link() {
let dynamic_libs = if env::consts::OS == "windows" {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let dynamic_libs = if target_os == "windows" {
[
"nssutil3.dll",
"nss3.dll",
Expand All @@ -198,12 +232,13 @@ fn dynamic_link() {
}

fn static_link() {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let mut static_libs = vec![
"certdb",
"certhi",
"cryptohi",
"freebl_static",
if env::consts::OS == "windows" {
if target_os == "windows" {
"libnspr4"
} else {
"nspr4"
Expand All @@ -215,12 +250,12 @@ fn static_link() {
"nsspki",
"nssutil",
"pk11wrap_static",
if env::consts::OS == "windows" {
if target_os == "windows" {
"libplc4"
} else {
"plc4"
},
if env::consts::OS == "windows" {
if target_os == "windows" {
"libplds4"
} else {
"plds4"
Expand All @@ -230,7 +265,7 @@ fn static_link() {
];
// macOS always dynamically links against the system sqlite library.
// See https://github.com/nss-dev/nss/blob/a8c22d8fc0458db3e261acc5e19b436ab573a961/coreconf/Darwin.mk#L130-L135
if env::consts::OS == "macos" {
if target_os == "macos" {
println!("cargo:rustc-link-lib=dylib=sqlite3");
} else {
static_libs.push("sqlite");
Expand Down Expand Up @@ -263,11 +298,7 @@ fn static_link() {
fn get_includes(nsstarget: &Path, nssdist: &Path) -> Vec<PathBuf> {
let nsprinclude = nsstarget.join("include").join("nspr");
let nssinclude = nssdist.join("public").join("nss");
let includes = vec![nsprinclude, nssinclude];
for i in &includes {
println!("cargo:include={}", i.to_str().unwrap());
}
includes
vec![nsprinclude, nssinclude]
}

fn build_bindings(base: &str, bindings: &Bindings, flags: &[String], gecko: bool) {
Expand All @@ -285,14 +316,15 @@ fn build_bindings(base: &str, bindings: &Bindings, flags: &[String], gecko: bool
builder = builder.clang_arg("-v");

if !gecko {
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
builder = builder.clang_arg("-DNO_NSPR_10_SUPPORT");
if env::consts::OS == "windows" {
if target_os == "windows" {
builder = builder.clang_arg("-DWIN");
} else if env::consts::OS == "macos" {
} else if target_os == "macos" {
builder = builder.clang_arg("-DDARWIN");
} else if env::consts::OS == "linux" {
} else if target_os == "linux" {
builder = builder.clang_arg("-DLINUX");
} else if env::consts::OS == "android" {
} else if target_os == "android" {
builder = builder.clang_arg("-DLINUX");
builder = builder.clang_arg("-DANDROID");
}
Expand Down Expand Up @@ -365,11 +397,9 @@ fn pkg_config() -> Result<Vec<String>, Box<dyn Error>> {

let mut flags: Vec<String> = Vec::new();

for f in cfg_str.split(' ') {
if let Some(include) = f.strip_prefix("-I") {
for f in cfg_str.split_whitespace() {
Comment thread
larseggert marked this conversation as resolved.
if f.starts_with("-I") {
flags.push(String::from(f));

println!("cargo:include={include}");
} else if let Some(path) = f.strip_prefix("-L") {
println!("cargo:rustc-link-search=native={path}");
} else if let Some(lib) = f.strip_prefix("-l") {
Expand All @@ -383,15 +413,12 @@ fn pkg_config() -> Result<Vec<String>, Box<dyn Error>> {
}

fn setup_standalone(nss_dir: String) -> Vec<String> {
setup_clang();

let nss = PathBuf::from(nss_dir);
println!("cargo:rerun-if-env-changed={}", nss.display());
println!("cargo:rerun-if-env-changed=NSS_DIR");
println!("cargo:rerun-if-env-changed=NSS_PREBUILT");

// $NSS_DIR/../dist/
let nssdist = nss.parent().unwrap().join("dist");
println!("cargo:rerun-if-env-changed={}", nssdist.display());
let nsstarget = "Release";

// If NSS_PREBUILT is set to a non-zero value, we assume that the NSS libraries are already
Expand All @@ -411,7 +438,7 @@ fn setup_standalone(nss_dir: String) -> Vec<String> {
if env::var("CARGO_CFG_FUZZING").is_ok()
|| env::var("PROFILE").unwrap_or_default() == "debug"
// FIXME: NSPR doesn't build proper dynamic libraries on Windows.
|| env::consts::OS == "windows"
|| env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows"
{
static_link();
} else {
Expand Down Expand Up @@ -537,7 +564,10 @@ fn process_config(config: &mut HashMap<String, Bindings>) {
}

fn main() {
println!("cargo:rerun-if-changed=src/min_version.rs");
println!("cargo:rerun-if-changed=min_version.txt");
println!("cargo:rustc-check-cfg=cfg(nss_nodb)");
setup_clang();

let flags = if cfg!(feature = "gecko") {
setup_for_gecko()
Expand Down
89 changes: 89 additions & 0 deletions src/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,95 @@ mod recprot {
write!(f, "[NULL AEAD]")
}
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
Comment thread
Not-Nik marked this conversation as resolved.
use super::{AEAD_NULL_TAG, RecordProtection};

fn aead() -> RecordProtection {
RecordProtection {}
}

#[test]
fn expansion() {
assert_eq!(aead().expansion(), AEAD_NULL_TAG.len());
}

#[test]
fn debug() {
assert_eq!(format!("{:?}", aead()), "[NULL AEAD]");
}

#[test]
fn encrypt_decrypt_roundtrip() {
let a = aead();
let plaintext = b"hello world";
let mut out = vec![0u8; plaintext.len() + a.expansion()];
let encrypted = a.encrypt(0, b"aad", plaintext, &mut out).unwrap();
assert_eq!(encrypted.len(), plaintext.len() + a.expansion());
assert_eq!(&encrypted[..plaintext.len()], plaintext);
assert_eq!(&encrypted[plaintext.len()..], AEAD_NULL_TAG);

let mut dec_out = vec![0u8; plaintext.len()];
let decrypted = a.decrypt(0, b"aad", encrypted, &mut dec_out).unwrap();
assert_eq!(decrypted, plaintext);
}

#[test]
fn encrypt_in_place_roundtrip() {
let a = aead();
let plaintext = b"hello";
let mut buf = plaintext.to_vec();
buf.resize(plaintext.len() + a.expansion(), 0);
let len = a.encrypt_in_place(0, b"", &mut buf).unwrap();
assert_eq!(len, buf.len());
assert_eq!(&buf[plaintext.len()..], AEAD_NULL_TAG);

let dec_len = a.decrypt_in_place(0, b"", &mut buf).unwrap();
assert_eq!(dec_len, plaintext.len());
assert_eq!(&buf[..dec_len], plaintext);
}

#[test]
fn decrypt_empty_plaintext() {
// Zero-length plaintext (just the tag) is valid.
let a = aead();
let mut out = vec![0u8; a.expansion()];
a.encrypt(0, b"", b"", &mut out).unwrap();
let mut dec = vec![];
let res = a.decrypt(0, b"", &out, &mut dec).unwrap();
assert_eq!(res, b"");
}

#[test]
fn decrypt_fails_too_short() {
let a = aead();
let short = &AEAD_NULL_TAG[..a.expansion() - 1];
assert!(a.decrypt(0, b"", short, &mut []).is_err());
Comment thread
Not-Nik marked this conversation as resolved.
}

#[test]
fn decrypt_fails_bad_tag() {
let a = aead();
let plaintext = b"test";
let mut buf = vec![0u8; plaintext.len() + a.expansion()];
a.encrypt(0, b"", plaintext, &mut buf).unwrap();
// Corrupt the tag.
let tag_start = plaintext.len();
buf[tag_start] ^= 0xff;
assert!(a.decrypt(0, b"", &buf, &mut []).is_err());
}

#[test]
fn decrypt_rejects_all_zero_data_bytes() {
// All-zero plaintext with correct tag should fail (looks like padding).
let a = aead();
let mut buf = vec![0u8; 4 + a.expansion()];
buf[4..].copy_from_slice(AEAD_NULL_TAG);
assert!(a.decrypt(0, b"", &buf, &mut []).is_err());
}
}
}

/// All the nonces are the same length. Exploit that.
Expand Down
14 changes: 14 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,17 @@ remap_enum! {
TLS_SIG_RSA_PSS_PSS_SHA512 = ssl_sig_rsa_pss_pss_sha512,
}
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::Epoch;

#[test]
fn epoch_into_usize() {
assert_eq!(usize::from(Epoch::Initial), 0);
assert_eq!(usize::from(Epoch::ZeroRtt), 1);
assert_eq!(usize::from(Epoch::Handshake), 2);
assert_eq!(usize::from(Epoch::ApplicationData), 3);
}
}
33 changes: 32 additions & 1 deletion src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub mod nspr {
include!(concat!(env!("OUT_DIR"), "/nspr_err.rs"));
}

#[expect(dead_code, reason = "Some constants are not used.")]
#[cfg_attr(not(test), expect(dead_code, reason = "Some constants are not used."))]
pub mod mozpkix {
// These are manually extracted from the many bindings generated
// by bindgen when provided with the simple header:
Expand Down Expand Up @@ -306,4 +306,35 @@ mod tests {
Error::String
);
}

#[test]
fn mozpkix_error_codes_are_negative() {
use crate::err::mozpkix::*;
let codes = [
MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE,
MOZILLA_PKIX_ERROR_CA_CERT_USED_AS_END_ENTITY,
MOZILLA_PKIX_ERROR_INADEQUATE_KEY_SIZE,
MOZILLA_PKIX_ERROR_V1_CERT_USED_AS_CA,
MOZILLA_PKIX_ERROR_NO_RFC822NAME_MATCH,
MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE,
MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE,
MOZILLA_PKIX_ERROR_SIGNATURE_ALGORITHM_MISMATCH,
MOZILLA_PKIX_ERROR_OCSP_RESPONSE_FOR_CERT_MISSING,
MOZILLA_PKIX_ERROR_VALIDITY_TOO_LONG,
MOZILLA_PKIX_ERROR_REQUIRED_TLS_FEATURE_MISSING,
MOZILLA_PKIX_ERROR_INVALID_INTEGER_ENCODING,
MOZILLA_PKIX_ERROR_EMPTY_ISSUER_NAME,
MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED,
MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT,
MOZILLA_PKIX_ERROR_MITM_DETECTED,
END_OF_LIST,
];
for (i, &code) in codes.iter().enumerate() {
assert_eq!(
code,
-16384 + i32::try_from(i).unwrap(),
"mozpkix code at index {i}"
);
}
}
}
Loading