0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 12:02:18 +02:00

Implement Nitro attestation

This commit is contained in:
moiseev-signal 2023-09-12 16:54:28 -07:00 committed by GitHub
parent ef542e3b9f
commit d464d0030b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 602 additions and 1 deletions

2
Cargo.lock generated
View File

@ -191,6 +191,7 @@ dependencies = [
"boring-sys",
"chacha20poly1305 0.10.1",
"chrono",
"ciborium",
"displaydoc",
"hex",
"hex-literal",
@ -206,6 +207,7 @@ dependencies = [
"sha2",
"snow",
"static_assertions",
"subtle",
"uuid",
"variant_count",
"x25519-dalek",

View File

@ -7,7 +7,7 @@
name = "attest"
version = "0.1.0"
authors = ["Signal Messenger LLC"]
edition = "2018"
edition = "2021"
license = "AGPL-3.0-only"
[dependencies]
@ -18,6 +18,7 @@ asn1 = "0.15.4"
bitflags = "2.3.3"
chacha20poly1305 = "0.10.1"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
displaydoc = "0.2"
hex = { version = "0.4", features = ["serde"] }
hex-literal = "0.4.1"
@ -32,6 +33,7 @@ serde_json = { version = "1.0", features = ["raw_value"] }
sha2 = "0.10"
snow = { version = "0.9.3", default-features = false }
static_assertions = "1.1"
subtle = "2.5"
uuid = "1.1.2"
variant_count = "1.1"
x25519-dalek = "2.0.0"

View File

@ -10,6 +10,7 @@ pub mod hsm_enclave;
pub mod ias;
pub mod sgx_session;
pub mod svr2;
pub mod svr3;
mod endian;
mod error;

5
rust/attest/src/svr3.rs Normal file
View File

@ -0,0 +1,5 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub mod nitro;

View File

@ -0,0 +1,591 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use boring::bn::BigNum;
use boring::ecdsa::EcdsaSig;
use boring::stack;
use boring::x509::store::X509StoreBuilder;
use boring::x509::{X509StoreContext, X509};
use ciborium::value::{Integer, Value};
use sha2::{Digest, Sha384};
use std::collections::HashMap;
use std::time::SystemTime;
use subtle::ConstantTimeEq;
const PUBLIC_KEY_LENGTH: usize = 32;
pub type PublicKeyBytes = [u8; PUBLIC_KEY_LENGTH];
pub fn attest(
evidence: &[u8],
expected_pcrs: &HashMap<usize, Vec<u8>>,
now: SystemTime,
) -> Result<PublicKeyBytes, NitroError> {
let cose_sign1 = CoseSign1::from_bytes(evidence)?;
let doc = cose_sign1.extract_attestation_doc(now)?;
doc.extract_public_key(expected_pcrs)
}
#[derive(Debug, displaydoc::Display, PartialEq, Eq)]
pub enum NitroError {
/// Invalid CBOR
InvalidCbor,
/// Invalid COSE_Sign1
InvalidCoseSign1,
/// Invalid signature
InvalidSignature,
/// Invalid attestation document
InvalidAttestationDoc,
/// Invalid certificate: {0}
InvalidCertificate(String),
/// Invalid PCRs
InvalidPcrs,
/// Invalid Public Key
InvalidPublicKey,
}
impl std::error::Error for NitroError {}
impl From<ciborium::de::Error<std::io::Error>> for NitroError {
fn from(_err: ciborium::de::Error<std::io::Error>) -> NitroError {
NitroError::InvalidCbor
}
}
impl From<boring::error::ErrorStack> for NitroError {
fn from(err: boring::error::ErrorStack) -> NitroError {
NitroError::InvalidCertificate(err.to_string())
}
}
#[derive(Debug)]
pub struct CoseSign1 {
protected_header: Vec<u8>,
// nitro has no unprotected header
payload: Vec<u8>,
signature: Vec<u8>,
}
impl CoseSign1 {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, NitroError> {
let value: Value = ciborium::from_reader(bytes)?;
value.try_into()
}
pub fn extract_attestation_doc(&self, now: SystemTime) -> Result<AttestationDoc, NitroError> {
let hash = Sha384::digest(self.to_canonical());
let r = BigNum::from_slice(&self.signature[..48]).expect("can extract r");
let s = BigNum::from_slice(&self.signature[48..]).expect("can extract s");
let sig = EcdsaSig::from_private_components(r, s).expect("can initialize signature");
let doc = AttestationDoc::from_bytes(self.payload.as_slice()).expect("can parse doc");
let cert = doc.verified_cert(now)?;
let key = cert
.public_key()
.and_then(|pub_key| pub_key.ec_key())
.expect("has EC key");
let is_valid = sig.verify(hash.as_slice(), &key).expect("can verify");
if !is_valid {
return Err(NitroError::InvalidSignature);
}
Ok(doc)
}
fn validating_new(
protected_header: &[u8],
payload: &[u8],
signature: &[u8],
) -> Result<Self, NitroError> {
let is_valid = {
let mut is_valid = true;
is_valid &= Self::is_valid_protected_header(protected_header);
is_valid &= (1..16384).contains(&payload.len());
is_valid &= signature.len() == 96;
is_valid
};
if !is_valid {
return Err(NitroError::InvalidCoseSign1);
}
Ok(CoseSign1 {
protected_header: protected_header.to_vec(),
payload: payload.to_vec(),
signature: signature.to_vec(),
})
}
fn is_valid_protected_header(bytes: &[u8]) -> bool {
let signing_algorithm: Integer = Integer::from(1);
let ecdsa_sha_384: Integer = Integer::from(-35);
let value: Value = ciborium::from_reader(bytes).expect("valid cbor");
match value {
Value::Map(vec) => match &vec[..] {
[(Value::Integer(key), Value::Integer(val))] => {
key == &signing_algorithm && val == &ecdsa_sha_384
}
_ => false,
},
_ => false,
}
}
fn to_canonical(&self) -> Vec<u8> {
let value = Value::Array(vec![
Value::Text("Signature1".to_string()),
Value::Bytes(self.protected_header.clone()),
Value::Bytes(vec![]),
Value::Bytes(self.payload.clone()),
]);
let mut bytes = Vec::with_capacity(self.protected_header.len() + self.payload.len());
ciborium::into_writer(&value, &mut bytes).expect("can write bytes");
bytes
}
}
impl TryFrom<Value> for CoseSign1 {
type Error = NitroError;
// Assumes tagged CBOR encoding of COSE_Sign1
fn try_from(value: Value) -> Result<CoseSign1, NitroError> {
if let Value::Array(parts) = value {
match &parts[..] {
[Value::Bytes(protected_header), Value::Map(_), Value::Bytes(payload), Value::Bytes(signature)] => {
CoseSign1::validating_new(protected_header, payload, signature)
}
_ => Err(NitroError::InvalidCoseSign1),
}
} else {
Err(NitroError::InvalidCoseSign1)
}
}
}
#[derive(Debug)]
pub struct AttestationDoc {
pub module_id: String,
pub digest: String,
pub timestamp: i64,
pub pcrs: Vec<(usize, Vec<u8>)>,
certificate: Vec<u8>,
cabundle: Vec<Vec<u8>>,
public_key: Option<Vec<u8>>,
pub user_data: Option<Vec<u8>>,
pub nonce: Option<Vec<u8>>,
}
impl TryFrom<Value> for AttestationDoc {
type Error = NitroError;
fn try_from(value: Value) -> Result<AttestationDoc, NitroError> {
let map = AttestationDoc::parse_as_cbor_map(value)?;
Self::from_cbor_map(map)
}
}
type CborMap = HashMap<String, Value>;
impl AttestationDoc {
pub fn from_bytes(bytes: &[u8]) -> Result<AttestationDoc, NitroError> {
let value: Value = ciborium::from_reader(bytes)?;
value.try_into()
}
fn parse_as_cbor_map(value: Value) -> Result<CborMap, NitroError> {
let Value::Map(pairs) = value else {
return Err(NitroError::InvalidAttestationDoc);
};
pairs
.into_iter()
.map(|(k, v)| {
let k = k.as_text().ok_or(NitroError::InvalidAttestationDoc)?;
Ok((k.to_string(), v))
})
.collect()
}
fn from_cbor_map(mut map: CborMap) -> Result<AttestationDoc, NitroError> {
let module_id = map["module_id"]
.as_text()
.filter(|s| !s.is_empty())
.ok_or(NitroError::InvalidAttestationDoc)?
.to_string();
let digest = map["digest"]
.as_text()
.filter(|s| s == &"SHA384")
.ok_or(NitroError::InvalidAttestationDoc)?
.to_string();
let timestamp = map["timestamp"]
.as_integer()
.and_then(|integer| i64::try_from(integer).ok())
.filter(|i| i.is_positive())
.ok_or(NitroError::InvalidAttestationDoc)?;
let pcrs: Vec<(usize, Vec<u8>)> = map
.get_mut("pcrs")
.ok_or(NitroError::InvalidAttestationDoc)?
.as_map_mut()
.and_then(|pairs| {
if !(1..=32).contains(&pairs.len()) {
return None;
}
let mut pcrs = Vec::with_capacity(pairs.len());
for (key, value) in pairs.iter() {
let index = key
.as_integer()
.and_then(|n| usize::try_from(n).ok())
.filter(|n| (0..32).contains(n))?;
let bytes = value
.as_bytes()
.filter(|bs| [32, 48, 64].contains(&bs.len()))?;
pcrs.push((index, bytes.to_vec()))
}
Some(pcrs)
})
.ok_or(NitroError::InvalidAttestationDoc)?;
let certificate = map["certificate"]
.as_bytes()
.map(|bs| bs.to_vec())
.filter(|bs| (1..=1024).contains(&bs.len()))
.ok_or(NitroError::InvalidAttestationDoc)?;
let cabundle = map["cabundle"]
.as_array()
.and_then(|vals| {
if vals.is_empty() {
return None;
}
let mut certs = Vec::with_capacity(vals.len());
for val in vals.iter() {
let bytes = val.as_bytes().filter(|bs| (1..=1024).contains(&bs.len()))?;
certs.push(bytes.to_vec());
}
Some(certs)
})
.ok_or(NitroError::InvalidAttestationDoc)?;
let public_key = map["public_key"].as_bytes().map(|bs| bs.to_vec());
if let Some(pk) = &public_key {
if pk.len() > 1024 {
return Err(NitroError::InvalidAttestationDoc);
}
}
let user_data = map["user_data"].as_bytes().map(|bs| bs.to_vec());
if let Some(data) = &user_data {
if data.len() > 512 {
return Err(NitroError::InvalidAttestationDoc);
}
}
let nonce = map["nonce"].as_bytes().map(|bs| bs.to_vec());
if let Some(nonce) = &nonce {
if nonce.len() > 512 {
return Err(NitroError::InvalidAttestationDoc);
}
}
Ok(AttestationDoc {
module_id,
digest,
timestamp,
pcrs,
certificate,
cabundle,
public_key,
user_data,
nonce,
})
}
fn verified_cert(&self, now: SystemTime) -> Result<X509, NitroError> {
let mut context = X509StoreContext::new()?;
let certificate = X509::from_der(&self.certificate)?;
let mut stack = stack::Stack::<X509>::new()?;
for der in self.cabundle.iter() {
let cert = X509::from_der(der)?;
stack.push(cert)?;
}
let stack = stack;
let trust = {
let root = X509::from_pem(ROOT_CERTIFICATE_PEM)?;
let mut builder = X509StoreBuilder::new()?;
builder.param_mut().set_time(
now.duration_since(SystemTime::UNIX_EPOCH)
.expect("current time is after 1970")
.as_secs()
.try_into()
.expect("haven't yet overflowed time_t"),
);
builder.add_cert(root)?;
builder.build()
};
let is_valid = context.init(&trust, &certificate, &stack, |ctx| ctx.verify_cert())?;
if !is_valid {
let message = context.error().to_string();
return Err(NitroError::InvalidCertificate(message));
}
Ok(certificate)
}
pub fn extract_public_key(
&self,
expected_pcrs: &HashMap<usize, Vec<u8>>,
) -> Result<PublicKeyBytes, NitroError> {
let mut is_match = true;
for (index, pcr) in self.pcrs.iter() {
is_match &= expected_pcrs
.get(index)
.map(|expected| expected.ct_eq(pcr).into())
// if the index is missing from the expected_pcrs we do not check it
.unwrap_or(true);
}
if !is_match {
return Err(NitroError::InvalidPcrs);
}
self.public_key
.clone()
.ok_or(NitroError::InvalidPublicKey)?
.try_into()
.map_err(|_| NitroError::InvalidPublicKey)
}
}
#[cfg(test)]
mod test {
use super::*;
use hex_literal::hex;
use std::time::Duration;
#[test]
fn test_extract_attestation_doc() {
let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(1684362463);
let cose_sign1 = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
cose_sign1
.extract_attestation_doc(timestamp)
.expect("valid signature");
}
#[test]
fn test_attestation() {
let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(1684948138);
let _pk = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_2)
.expect("can parse")
.extract_attestation_doc(timestamp)
.expect("valid signature")
.extract_public_key(&get_test_pcrs())
.expect("valid pcrs");
}
#[test]
fn test_expired_cert() {
let cose_sign1 = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
let err = cose_sign1
.extract_attestation_doc(SystemTime::now())
.unwrap_err();
assert!(format!("{err:?}").contains("expired"));
}
#[test]
fn test_not_yet_valid_cert() {
let cose_sign1 = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
let err = cose_sign1
.extract_attestation_doc(SystemTime::UNIX_EPOCH)
.unwrap_err();
assert!(format!("{err:?}").contains("not yet valid"));
}
#[test]
fn test_invalid_signature() {
let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(1684362463);
let mut cose_sign1 = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
cose_sign1.signature[0] ^= 0xff;
let err = cose_sign1.extract_attestation_doc(timestamp).unwrap_err();
assert_eq!(NitroError::InvalidSignature, err);
}
fn invalid_cose_sign1_test<F>(mut f: F)
where
F: FnMut(&mut CoseSign1),
{
let mut subject = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
f(&mut subject);
let err = CoseSign1::validating_new(
&subject.protected_header,
&subject.payload,
&subject.signature,
)
.unwrap_err();
assert_eq!(NitroError::InvalidCoseSign1, err);
}
#[test]
fn test_invalid_cose_sign1_signature_len() {
invalid_cose_sign1_test(|subject| subject.signature.push(0x00));
}
#[test]
fn test_invalid_cose_sign1_empty_payload() {
invalid_cose_sign1_test(|subject| subject.payload = vec![]);
}
#[test]
fn test_invalid_cose_sign1_payload_too_large() {
invalid_cose_sign1_test(|subject| subject.payload = [0; 16384].to_vec());
}
#[test]
fn test_invalid_cose_sign1_invalid_header() {
invalid_cose_sign1_test(|subject| subject.protected_header = vec![1, 2, 3]);
}
#[test]
fn test_canonical_serialization() {
let subject = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("can parse");
assert_eq!(subject.to_canonical(), VALID_DOCUMENT_BYTES_1_CANONICAL);
}
#[test]
fn test_non_string_keys() {
let value: Value = Value::Map(vec![(Value::Integer(42.into()), Value::Integer(42.into()))]);
let err =
AttestationDoc::parse_as_cbor_map(value).expect_err("Should have failed validation");
assert_eq!(err, NitroError::InvalidAttestationDoc);
}
fn invalid_attestation_doc_test<F>(mut f: F)
where
F: FnMut(&mut CborMap),
{
let cose_sign1 = CoseSign1::from_bytes(VALID_DOCUMENT_BYTES_1).expect("valid cose_sign1");
let value: Value =
ciborium::from_reader(cose_sign1.payload.as_slice()).expect("valid cbor");
let mut map = AttestationDoc::parse_as_cbor_map(value).expect("valid cbor map");
f(&mut map);
let err = AttestationDoc::from_cbor_map(map).unwrap_err();
assert_eq!(NitroError::InvalidAttestationDoc, err);
}
#[test]
fn test_empty_module_id() {
invalid_attestation_doc_test(|map| {
*map.get_mut("module_id").unwrap() = Value::Text("".to_string());
});
}
#[test]
fn test_invalid_digest() {
invalid_attestation_doc_test(|map| {
*map.get_mut("digest").unwrap() = Value::Text("not sha384".to_string());
});
}
#[test]
fn test_zero_timestamp() {
invalid_attestation_doc_test(|map| {
*map.get_mut("timestamp").unwrap() = Value::Integer(0.into());
});
}
#[test]
fn test_empty_pcrs() {
invalid_attestation_doc_test(|map| {
*map.get_mut("pcrs").unwrap() = Value::Array(vec![]);
});
}
#[test]
fn test_too_many_pcrs() {
invalid_attestation_doc_test(|map| {
*map.get_mut("pcrs").unwrap() = Value::Array(
(1..33)
// Should be a byte array, but any Value would do for length validation
.map(|i| Value::Integer(i.into()))
.collect(),
);
});
}
#[test]
fn test_invalid_pcr_index() {
invalid_attestation_doc_test(|map| {
let pcrs = map.get_mut("pcrs").unwrap();
let pcr = pcrs.as_map_mut().unwrap();
pcr[0] = (Value::Integer(32.into()), pcr[0].1.clone());
});
}
#[test]
fn test_invalid_pcr_length() {
invalid_attestation_doc_test(|map| {
let pcrs = map.get_mut("pcrs").unwrap();
let pcr = pcrs.as_map_mut().unwrap();
pcr[0] = (pcr[0].0.clone(), Value::Bytes(b"00010203".to_vec()));
});
}
#[test]
fn test_empty_certificate() {
invalid_attestation_doc_test(|map| {
*map.get_mut("certificate").unwrap() = Value::Bytes(vec![]);
});
}
#[test]
fn test_certificate_too_long() {
invalid_attestation_doc_test(|map| {
*map.get_mut("certificate").unwrap() = Value::Bytes([0; 1025].to_vec());
});
}
#[test]
fn test_public_key_too_long() {
invalid_attestation_doc_test(|map| {
map.insert("public_key".to_string(), Value::Bytes([0; 1025].to_vec()));
});
}
#[test]
fn test_user_data_too_long() {
invalid_attestation_doc_test(|map| {
map.insert("user_data".to_string(), Value::Bytes([0; 513].to_vec()));
});
}
#[test]
fn test_nonce_too_long() {
invalid_attestation_doc_test(|map| {
map.insert("nonce".to_string(), Value::Bytes([0; 513].to_vec()));
});
}
const VALID_DOCUMENT_BYTES_1: &[u8] = include_bytes!("../../tests/data/test_cose_sign1_01.dat");
const VALID_DOCUMENT_BYTES_1_CANONICAL: &[u8] =
include_bytes!("../../tests/data/cose_sign1_canonical.dat");
const VALID_DOCUMENT_BYTES_2: &[u8] = include_bytes!("../../tests/data/test_cose_sign1_02.dat");
fn get_test_pcrs() -> HashMap<usize, Vec<u8>> {
let mut map = HashMap::<usize, _>::new();
map.insert(0, hex!("28de6557cce896cf8c580d8674fbc13c45c1a7636545ef022a01007336b8752b9a1cd9ce69df2ecacc7696a1203b45c2").to_vec());
map.insert(1, hex!("52b919754e1643f4027eeee8ec39cc4a2cb931723de0c93ce5cc8d407467dc4302e86490c01c0d755acfe10dbf657546").to_vec());
map.insert(2, hex!("3bc780f5f2adc596f55c5d8b85760f1e9e585c7016957673616c0611280c4b99c8877caff00d70567a96979abe59dc0a").to_vec());
map.insert(3, hex!("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec());
map.insert(4, hex!("17354aa3f163d6882a4ff746e5821c5be66f1658472feac83268cf4b7461015ea47993c07025ebb5e134cbc13b16ac97").to_vec());
map
}
}
// From https://aws-nitro-enclaves.amazonaws.com/AWS_NitroEnclaves_Root-G1.zip
// Zip hash (SHA256): 8cf60e2b2efca96c6a9e71e851d00c1b6991cc09eadbe64a6a1d1b1eb9faff7c
const ROOT_CERTIFICATE_PEM: &[u8] = br"
-----BEGIN CERTIFICATE-----
MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD
VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4
MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL
DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG
BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb
48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE
h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF
R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC
MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW
rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N
IwLz3/Y=
-----END CERTIFICATE-----
";

Binary file not shown.

Binary file not shown.

Binary file not shown.