diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7fdba4..df021db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,19 +17,19 @@ jobs: sudo apt-get update -qq sudo apt-get install -qq clang gcc-arm-none-eabi gcc-powerpc-linux-gnu \ llvm scons - - name: Linux Host Build + - name: Linux Build run: | scons --jobs "$(nproc)" - - name: Linux Host Build -march=penryn + - name: Linux Build -march=penryn run: | scons --jobs "$(nproc)" --host-march=penryn - - name: Linux Host Build -march=nehalem + - name: Linux Build -march=nehalem run: | scons --jobs "$(nproc)" --host-march=nehalem - - name: Linux Host Build -march=skylake + - name: Linux Build -march=skylake run: | scons --jobs "$(nproc)" --host-march=skylake - - name: Linux Host Build with Sanitizers + - name: Linux Build with Sanitizers run: | scons --jobs "$(nproc)" --sanitize - name: arm-eabi Cross Build @@ -38,6 +38,11 @@ jobs: - name: powerpc-linux Cross Build run: | scons --jobs "$(nproc)" --target=powerpc-linux + - name: Linux Rust Build + run: | + rustup update + cargo check --all-targets + cargo test macos-test: name: macOS Test diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..44b6aab --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +group_imports = "StdExternalCrate" +imports_granularity = "Crate" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..61b1ce5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "liblithium" +version = "0.1.0" +authors = ["Chris Copeland "] +description = "A lightweight and portable cryptography library" +license = "Apache-2.0" +repository = "https://github.com/teslamotors/liblithium" +documentation = "https://docs.rs/liblithium" +keywords = ["x25519", "gimli", "signatures"] +categories = ["cryptography", "embedded", "no-std"] +edition = "2021" +rust-version = "1.82.0" +build = "rust/build.rs" +autobins = false +autoexamples = false +include = ["/src/", "/include/", "/rust/", "!SCons*", "!Dockerfile", "!*.bash"] + +[lib] +path = "rust/src/lib.rs" + +[build-dependencies] +bindgen = "0.71" +cc = { version = "1.2", features = ["parallel"] } + +[profile.release] +debug = true + +[[bin]] +name = "gimli-hash" +path = "rust/bin/hash.rs" + +[[bin]] +name = "lith-keygen" +path = "rust/bin/keygen.rs" + +[[bin]] +name = "lith-sign" +path = "rust/bin/sign.rs" + +[[bin]] +name = "lith-verify" +path = "rust/bin/verify.rs" diff --git a/rust/bin/hash.rs b/rust/bin/hash.rs new file mode 100644 index 0000000..7bff089 --- /dev/null +++ b/rust/bin/hash.rs @@ -0,0 +1,30 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + env, + fs::read, + io::{stdin, Read}, +}; + +use liblithium::gimli_hash::{hash_hex_string, DEFAULT_LEN}; + +fn main() -> std::io::Result<()> { + let mut args = env::args(); + let _prog = args.next(); + if args.len() < 1 { + let mut bytes = Vec::new(); + stdin().read_to_end(&mut bytes)?; + println!("{} -", hash_hex_string::(bytes.as_slice())); + } else { + for path in args { + let bytes = read(&path)?; + println!( + "{} {}", + hash_hex_string::(bytes.as_slice()), + path + ); + } + } + Ok(()) +} diff --git a/rust/bin/keygen.rs b/rust/bin/keygen.rs new file mode 100644 index 0000000..6ffa12d --- /dev/null +++ b/rust/bin/keygen.rs @@ -0,0 +1,23 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + env, + fs::File, + io::{Result, Write}, + process::exit, +}; + +use liblithium::sign::keygen; + +fn main() -> Result<()> { + let mut args = env::args(); + let prog = args.next().unwrap(); + let sk_path = args.next().unwrap_or_else(|| { + eprintln!("usage: {} ", prog); + exit(1) + }); + let (pk, sk) = keygen(); + File::create(&sk_path)?.write_all(&sk)?; + File::create(sk_path + ".pub")?.write_all(&pk) +} diff --git a/rust/bin/sign.rs b/rust/bin/sign.rs new file mode 100644 index 0000000..5f69842 --- /dev/null +++ b/rust/bin/sign.rs @@ -0,0 +1,35 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + env, + fs::{read, File}, + io::Write, + process::exit, +}; + +use liblithium::sign::create; + +fn main() -> std::io::Result<()> { + let mut args = env::args(); + let prog = args.next().unwrap(); + if args.len() != 3 { + eprintln!( + "usage: {} ", + prog + ); + exit(1); + } + let sk_path = args.next().unwrap(); + let msg_path = args.next().unwrap(); + let sig_path = args.next().unwrap(); + + let sk: [u8; liblithium::sign::SECRET_KEY_LEN] = read(sk_path)? + .as_slice() + .try_into() + .expect("incorrect secret key length"); + let msg = read(msg_path)?; + + let sig = create(&msg, &sk); + File::create(sig_path)?.write_all(&sig) +} diff --git a/rust/bin/verify.rs b/rust/bin/verify.rs new file mode 100644 index 0000000..39fe2ee --- /dev/null +++ b/rust/bin/verify.rs @@ -0,0 +1,37 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::{env, fs::read, process::exit}; + +use liblithium::sign::verify; + +fn main() -> std::io::Result<()> { + let mut args = env::args(); + let prog = args.next().unwrap(); + if args.len() != 3 { + eprintln!( + "usage: {} ", + prog + ); + exit(1); + } + let pk_path = args.next().unwrap(); + let msg_path = args.next().unwrap(); + let sig_path = args.next().unwrap(); + + let pk: [u8; liblithium::sign::PUBLIC_KEY_LEN] = read(pk_path)? + .as_slice() + .try_into() + .expect("incorrect secret key length"); + let sig: [u8; liblithium::sign::SIGN_LEN] = read(sig_path)? + .as_slice() + .try_into() + .expect("incorrect signature length"); + let msg = read(msg_path)?; + + if !verify(msg.as_slice(), &sig, &pk) { + eprintln!("could not verify signature"); + exit(1); + } + Ok(()) +} diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..91793a9 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,53 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::{env, path::PathBuf}; + +static SOURCES: &[&str] = &[ + "fe.c", + "gimli.c", + "gimli_aead.c", + "gimli_common.c", + "gimli_hash.c", + "memzero.c", + "random.c", + "sign.c", + "x25519.c", +]; + +fn main() { + let root_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let src_dir = root_dir.join("src"); + let include_dir = root_dir.join("include"); + + let mut cc = cc::Build::new(); + cc.files(SOURCES.iter().map(|s| src_dir.join(s))); + cc.include(&include_dir); + cc.flag("-ansi"); + cc.compile("liblithium"); + + let wrapper = "rust/wrapper.h"; + + println!("cargo:rerun-if-changed={}", wrapper); + for d in [&src_dir, &include_dir] { + println!("cargo:rerun-if-changed={}", d.display()); + } + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + + bindgen::builder() + .header(wrapper) + .clang_arg(format!("-I{}", include_dir.display())) + .disable_nested_struct_naming() + .use_core() + .default_enum_style(bindgen::EnumVariation::NewType { + is_bitfield: false, + is_global: false, + }) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Failed to generate bindings.") + .write_to_file(out_dir.join("bindings.rs")) + .expect("Failed to write bindings."); +} diff --git a/rust/src/bindings.rs b/rust/src/bindings.rs new file mode 100644 index 0000000..1033c58 --- /dev/null +++ b/rust/src/bindings.rs @@ -0,0 +1,9 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(unused)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/rust/src/gimli_hash.rs b/rust/src/gimli_hash.rs new file mode 100644 index 0000000..aae1f28 --- /dev/null +++ b/rust/src/gimli_hash.rs @@ -0,0 +1,76 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::mem::MaybeUninit; + +use crate::bindings::{ + gimli_hash_final, gimli_hash_init, gimli_hash_state, gimli_hash_update, GIMLI_HASH_DEFAULT_LEN, +}; + +/// An object for incrementally hashing a message. +#[repr(transparent)] +pub struct GimliHash { + state: gimli_hash_state, +} + +pub const DEFAULT_LEN: usize = GIMLI_HASH_DEFAULT_LEN as usize; + +impl GimliHash { + pub fn new() -> GimliHash { + let mut h = MaybeUninit::::uninit(); + unsafe { + gimli_hash_init(&raw mut (*h.as_mut_ptr()).state); + h.assume_init() + } + } + + pub fn update(&mut self, data: &[u8]) { + unsafe { gimli_hash_update(&mut self.state, data.as_ptr(), data.len()) } + } + + pub fn final_(mut self) -> [u8; N] { + let mut h = MaybeUninit::<[u8; N]>::uninit(); + unsafe { + gimli_hash_final(&mut self.state, &raw mut (*h.as_mut_ptr())[0], N); + h.assume_init() + } + } +} + +impl Default for GimliHash { + fn default() -> Self { + Self::new() + } +} + +/// Compute an N-byte GimliHash of data in one shot. +pub fn gimli_hash(data: &[u8]) -> [u8; N] { + let mut g = GimliHash::new(); + g.update(data); + g.final_() +} + +#[doc(hidden)] +pub fn hash_hex_string(data: &[u8]) -> String { + use std::fmt::Write; + + let mut s = String::with_capacity(N * 2); + let h = gimli_hash::(data); + for b in h { + write!(s, "{:02x}", b).unwrap(); + } + s +} + +#[cfg(test)] +mod test { + use crate::gimli_hash::{hash_hex_string, DEFAULT_LEN}; + + #[test] + fn test_gimli_hash() { + assert_eq!( + hash_hex_string::(&[]), + "27ae20e95fbc2bf01e972b0015eea431c20fc8818f25bc6dbe66232230db352f" + ); + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..a635479 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,7 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +mod bindings; + +pub mod gimli_hash; +pub mod sign; diff --git a/rust/src/sign.rs b/rust/src/sign.rs new file mode 100644 index 0000000..4a958c2 --- /dev/null +++ b/rust/src/sign.rs @@ -0,0 +1,148 @@ +// Part of liblithium, under the Apache License v2.0. +// SPDX-License-Identifier: Apache-2.0 + +use std::mem::MaybeUninit; + +use crate::bindings::{ + lith_sign_create_from_prehash, lith_sign_final_create, lith_sign_final_prehash, + lith_sign_final_verify, lith_sign_init, lith_sign_keygen, lith_sign_state, lith_sign_update, + lith_sign_verify_prehash, LITH_SIGN_LEN, LITH_SIGN_PREHASH_LEN, LITH_SIGN_PUBLIC_KEY_LEN, + LITH_SIGN_SECRET_KEY_LEN, +}; + +/// An object for computing or verifying a lithium signature. +#[repr(transparent)] +pub struct Sign { + state: lith_sign_state, +} + +pub const SECRET_KEY_LEN: usize = LITH_SIGN_SECRET_KEY_LEN as usize; +pub const SIGN_LEN: usize = LITH_SIGN_LEN as usize; +pub const PUBLIC_KEY_LEN: usize = LITH_SIGN_PUBLIC_KEY_LEN as usize; +pub const PREHASH_LEN: usize = LITH_SIGN_PREHASH_LEN as usize; + +impl Sign { + pub fn new() -> Sign { + let mut s = MaybeUninit::::uninit(); + unsafe { + lith_sign_init(&raw mut (*s.as_mut_ptr()).state); + s.assume_init() + } + } + + pub fn update(&mut self, data: &[u8]) { + unsafe { + lith_sign_update(&mut self.state, data.as_ptr(), data.len()); + } + } + + pub fn final_create(&mut self, secret_key: &[u8; SECRET_KEY_LEN]) -> [u8; SIGN_LEN] { + let mut sig = MaybeUninit::<[u8; SIGN_LEN]>::uninit(); + unsafe { + lith_sign_final_create( + &mut self.state, + &raw mut (*sig.as_mut_ptr())[0], + secret_key.as_ptr(), + ); + sig.assume_init() + } + } + + pub fn final_verify(mut self, sig: &[u8; SIGN_LEN], public_key: &[u8; PUBLIC_KEY_LEN]) -> bool { + unsafe { lith_sign_final_verify(&mut self.state, sig.as_ptr(), public_key.as_ptr()) } + } + + pub fn final_prehash(mut self) -> [u8; PREHASH_LEN] { + let mut prehash = MaybeUninit::<[u8; PREHASH_LEN]>::uninit(); + unsafe { + lith_sign_final_prehash(&mut self.state, &raw mut (*prehash.as_mut_ptr())[0]); + prehash.assume_init() + } + } +} + +impl Default for Sign { + fn default() -> Self { + Self::new() + } +} + +/// Generate a public and secret keypair. +pub fn keygen() -> ([u8; PUBLIC_KEY_LEN], [u8; SECRET_KEY_LEN]) { + let mut pk = MaybeUninit::<[u8; PUBLIC_KEY_LEN]>::uninit(); + let mut sk = MaybeUninit::<[u8; SECRET_KEY_LEN]>::uninit(); + unsafe { + lith_sign_keygen( + &raw mut (*pk.as_mut_ptr())[0], + &raw mut (*sk.as_mut_ptr())[0], + ); + (pk.assume_init(), sk.assume_init()) + } +} + +/// Create a signature from a prehash blob generated by `Sign::final_prehash`. +pub fn create_from_prehash( + prehash: &[u8; PREHASH_LEN], + secret_key: &[u8; SECRET_KEY_LEN], +) -> [u8; SIGN_LEN] { + let mut sig = MaybeUninit::<[u8; SIGN_LEN]>::uninit(); + unsafe { + lith_sign_create_from_prehash( + &raw mut (*sig.as_mut_ptr())[0], + prehash.as_ptr(), + secret_key.as_ptr(), + ); + sig.assume_init() + } +} + +/// Verify a signature on a prehash blob generated by `Sign::final_prehash`. +pub fn verify_prehash( + sig: &[u8; SIGN_LEN], + prehash: &[u8; PREHASH_LEN], + public_key: &[u8; PUBLIC_KEY_LEN], +) -> bool { + unsafe { lith_sign_verify_prehash(sig.as_ptr(), prehash.as_ptr(), public_key.as_ptr()) } +} + +/// Create a signature over data. +pub fn create(data: &[u8], secret_key: &[u8; SECRET_KEY_LEN]) -> [u8; SIGN_LEN] { + let mut s = Sign::new(); + s.update(data); + s.final_create(secret_key) +} + +/// Verify a signature over data. +pub fn verify(data: &[u8], sig: &[u8; SIGN_LEN], public_key: &[u8; PUBLIC_KEY_LEN]) -> bool { + let mut s = Sign::new(); + s.update(data); + s.final_verify(sig, public_key) +} + +#[cfg(test)] +mod test { + use super::{create, create_from_prehash, keygen, verify, verify_prehash, Sign}; + + #[test] + fn test_sign() { + let (pk, sk) = keygen(); + let mut data = vec![1, 2, 3]; + let sig = create(data.as_slice(), &sk); + assert!(verify(data.as_slice(), &sig, &pk)); + data[0] ^= 0xFF; + assert!(!verify(data.as_slice(), &sig, &pk)); + } + + #[test] + fn test_sign_prehash() { + let (pk, sk) = keygen(); + let data = vec![1, 2, 3]; + let mut s = Sign::new(); + s.update(data.as_slice()); + let mut prehash = s.final_prehash(); + let sig = create_from_prehash(&prehash, &sk); + assert!(verify_prehash(&sig, &prehash, &pk)); + prehash[0] ^= 0xFF; + assert!(!verify_prehash(&sig, &prehash, &pk)); + } +} diff --git a/rust/wrapper.h b/rust/wrapper.h new file mode 100644 index 0000000..0042f50 --- /dev/null +++ b/rust/wrapper.h @@ -0,0 +1,10 @@ +/* + * Part of liblithium, under the Apache License v2.0. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include