Compare commits
3 Commits
a02ef68a70
...
0bb6e596ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb6e596ac | ||
|
|
881f16bb1a | ||
|
|
78895bca5b |
@@ -172,15 +172,15 @@ pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell
|
|||||||
key.into()
|
key.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derives a dedicated key used only for user-agent pubkey integrity tags.
|
/// Derives a dedicated key used for integrity tags within a specific domain.
|
||||||
pub fn derive_useragent_integrity_key(seal_key: &mut KeyCell) -> KeyCell {
|
pub fn derive_integrity_key(seal_key: &mut KeyCell, derive_tag: &[u8]) -> KeyCell {
|
||||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||||
|
|
||||||
let mut derived = SafeCell::new(Key::default());
|
let mut derived = SafeCell::new(Key::default());
|
||||||
seal_key.0.read_inline(|seal_key_bytes| {
|
seal_key.0.read_inline(|seal_key_bytes| {
|
||||||
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(seal_key_bytes.as_ref())
|
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(seal_key_bytes.as_ref())
|
||||||
.expect("HMAC key initialization must not fail for 32-byte key");
|
.expect("HMAC key initialization must not fail for 32-byte key");
|
||||||
mac.update(USERAGENT_INTEGRITY_DERIVE_TAG);
|
mac.update(derive_tag);
|
||||||
let output = mac.finalize().into_bytes();
|
let output = mac.finalize().into_bytes();
|
||||||
|
|
||||||
let mut writer = derived.write();
|
let mut writer = derived.write();
|
||||||
@@ -191,25 +191,29 @@ pub fn derive_useragent_integrity_key(seal_key: &mut KeyCell) -> KeyCell {
|
|||||||
derived.into()
|
derived.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Computes an integrity tag for a user-agent pubkey DB entry.
|
/// Computes an integrity tag for a specific domain and payload shape.
|
||||||
pub fn compute_useragent_pubkey_integrity_tag(
|
pub fn compute_integrity_tag<'a, I>(
|
||||||
integrity_key: &mut KeyCell,
|
integrity_key: &mut KeyCell,
|
||||||
key_type_discriminant: i32,
|
purpose_tag: &[u8],
|
||||||
public_key: &[u8],
|
data_parts: I,
|
||||||
) -> [u8; 32] {
|
) -> [u8; 32]
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a [u8]>,
|
||||||
|
{
|
||||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||||
|
|
||||||
let mut tag = [0u8; 32];
|
let mut output_tag = [0u8; 32];
|
||||||
integrity_key.0.read_inline(|integrity_key_bytes| {
|
integrity_key.0.read_inline(|integrity_key_bytes| {
|
||||||
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(integrity_key_bytes.as_ref())
|
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(integrity_key_bytes.as_ref())
|
||||||
.expect("HMAC key initialization must not fail for 32-byte key");
|
.expect("HMAC key initialization must not fail for 32-byte key");
|
||||||
mac.update(USERAGENT_INTEGRITY_TAG);
|
mac.update(purpose_tag);
|
||||||
mac.update(&key_type_discriminant.to_be_bytes());
|
for data_part in data_parts {
|
||||||
mac.update(public_key);
|
mac.update(data_part);
|
||||||
tag.copy_from_slice(&mac.finalize().into_bytes());
|
}
|
||||||
|
output_tag.copy_from_slice(&mac.finalize().into_bytes());
|
||||||
});
|
});
|
||||||
|
|
||||||
tag
|
output_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -285,22 +289,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn useragent_integrity_tag_deterministic() {
|
pub fn integrity_tag_deterministic() {
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||||
let mut integrity_key = derive_useragent_integrity_key(&mut seal_key);
|
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||||
let t1 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey");
|
let key_type = 1i32.to_be_bytes();
|
||||||
let t2 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey");
|
let t1 = compute_integrity_tag(
|
||||||
|
&mut integrity_key,
|
||||||
|
USERAGENT_INTEGRITY_TAG,
|
||||||
|
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||||
|
);
|
||||||
|
let t2 = compute_integrity_tag(
|
||||||
|
&mut integrity_key,
|
||||||
|
USERAGENT_INTEGRITY_TAG,
|
||||||
|
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||||
|
);
|
||||||
assert_eq!(t1, t2);
|
assert_eq!(t1, t2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn useragent_integrity_tag_changes_with_key_type() {
|
pub fn integrity_tag_changes_with_payload() {
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||||
let mut integrity_key = derive_useragent_integrity_key(&mut seal_key);
|
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||||
let t1 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 1, b"pubkey");
|
let key_type_1 = 1i32.to_be_bytes();
|
||||||
let t2 = compute_useragent_pubkey_integrity_tag(&mut integrity_key, 2, b"pubkey");
|
let key_type_2 = 2i32.to_be_bytes();
|
||||||
|
let t1 = compute_integrity_tag(
|
||||||
|
&mut integrity_key,
|
||||||
|
USERAGENT_INTEGRITY_TAG,
|
||||||
|
[key_type_1.as_slice(), b"pubkey".as_ref()],
|
||||||
|
);
|
||||||
|
let t2 = compute_integrity_tag(
|
||||||
|
&mut integrity_key,
|
||||||
|
USERAGENT_INTEGRITY_TAG,
|
||||||
|
[key_type_2.as_slice(), b"pubkey".as_ref()],
|
||||||
|
);
|
||||||
assert_ne!(t1, t2);
|
assert_ne!(t1, t2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ enum State {
|
|||||||
Unsealed {
|
Unsealed {
|
||||||
root_key_history_id: i32,
|
root_key_history_id: i32,
|
||||||
root_key: KeyCell,
|
root_key: KeyCell,
|
||||||
useragent_integrity_key: KeyCell,
|
integrity_key: KeyCell,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +146,8 @@ impl KeyHolder {
|
|||||||
}
|
}
|
||||||
let salt = v1::generate_salt();
|
let salt = v1::generate_salt();
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||||
let useragent_integrity_key = v1::derive_useragent_integrity_key(&mut seal_key);
|
let integrity_key =
|
||||||
|
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||||
let mut root_key = KeyCell::new_secure_random();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
@@ -195,7 +196,7 @@ impl KeyHolder {
|
|||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key,
|
root_key,
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
useragent_integrity_key,
|
integrity_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Keyholder bootstrapped successfully");
|
info!("Keyholder bootstrapped successfully");
|
||||||
@@ -229,7 +230,8 @@ impl KeyHolder {
|
|||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?;
|
})?;
|
||||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||||
let useragent_integrity_key = v1::derive_useragent_integrity_key(&mut seal_key);
|
let integrity_key =
|
||||||
|
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
|
||||||
|
|
||||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||||
|
|
||||||
@@ -253,7 +255,7 @@ impl KeyHolder {
|
|||||||
error!(?err, "Broken database: invalid encryption key size");
|
error!(?err, "Broken database: invalid encryption key size");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?,
|
})?,
|
||||||
useragent_integrity_key,
|
integrity_key,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Keyholder unsealed successfully");
|
info!("Keyholder unsealed successfully");
|
||||||
@@ -261,25 +263,21 @@ impl KeyHolder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
// Signs a generic integrity payload using the vault-derived integrity key
|
||||||
#[message]
|
#[message]
|
||||||
pub fn sign_useragent_pubkey_integrity_tag(
|
pub fn sign_integrity_tag(
|
||||||
&mut self,
|
&mut self,
|
||||||
public_key: Vec<u8>,
|
purpose_tag: Vec<u8>,
|
||||||
key_type: models::KeyType,
|
data_parts: Vec<Vec<u8>>,
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
let State::Unsealed {
|
let State::Unsealed { integrity_key, .. } = &mut self.state else {
|
||||||
useragent_integrity_key,
|
|
||||||
..
|
|
||||||
} = &mut self.state
|
|
||||||
else {
|
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
};
|
};
|
||||||
|
|
||||||
let tag = v1::compute_useragent_pubkey_integrity_tag(
|
let tag = v1::compute_integrity_tag(
|
||||||
useragent_integrity_key,
|
integrity_key,
|
||||||
key_type as i32,
|
&purpose_tag,
|
||||||
&public_key,
|
data_parts.iter().map(Vec::as_slice),
|
||||||
);
|
);
|
||||||
Ok(tag.to_vec())
|
Ok(tag.to_vec())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ use super::Error;
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::{self, SignUseragentPubkeyIntegrityTag},
|
keyholder::{self, SignIntegrityTag},
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||||
},
|
},
|
||||||
db::schema,
|
db::schema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AttestationStatus {
|
||||||
|
Attested,
|
||||||
|
NotAttested,
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
}
|
}
|
||||||
@@ -133,8 +140,12 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||||
) -> Result<ChallengeContext, Self::Error> {
|
) -> Result<ChallengeContext, Self::Error> {
|
||||||
self.verify_pubkey_integrity_before_challenge(&pubkey)
|
match self.verify_pubkey_attestation_status(&pubkey).await? {
|
||||||
.await?;
|
AttestationStatus::Attested | AttestationStatus::Unavailable => {}
|
||||||
|
AttestationStatus::NotAttested => {
|
||||||
|
return Err(Error::InvalidChallengeSolution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let stored_bytes = pubkey.to_stored_bytes();
|
let stored_bytes = pubkey.to_stored_bytes();
|
||||||
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?;
|
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?;
|
||||||
@@ -257,9 +268,12 @@ where
|
|||||||
.conn
|
.conn
|
||||||
.actors
|
.actors
|
||||||
.key_holder
|
.key_holder
|
||||||
.ask(SignUseragentPubkeyIntegrityTag {
|
.ask(SignIntegrityTag {
|
||||||
public_key: pubkey.to_stored_bytes(),
|
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||||
key_type: pubkey.key_type(),
|
data_parts: vec![
|
||||||
|
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
||||||
|
pubkey.to_stored_bytes(),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -287,10 +301,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_pubkey_integrity_before_challenge(
|
async fn verify_pubkey_attestation_status(
|
||||||
&self,
|
&self,
|
||||||
pubkey: &AuthPublicKey,
|
pubkey: &AuthPublicKey,
|
||||||
) -> Result<(), Error> {
|
) -> Result<AttestationStatus, Error> {
|
||||||
let stored_tag: Option<Option<Vec<u8>>> = {
|
let stored_tag: Option<Option<Vec<u8>>> = {
|
||||||
let mut conn = self.conn.db.get().await.map_err(|e| {
|
let mut conn = self.conn.db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
@@ -316,19 +330,19 @@ where
|
|||||||
|
|
||||||
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
|
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
|
||||||
// Vault sealed/unbootstrapped: cannot verify integrity yet.
|
// Vault sealed/unbootstrapped: cannot verify integrity yet.
|
||||||
return Ok(());
|
return Ok(AttestationStatus::Unavailable);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(stored_tag) = stored_tag else {
|
match stored_tag {
|
||||||
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
|
Some(stored_tag) if stored_tag == expected_tag => Ok(AttestationStatus::Attested),
|
||||||
return Err(Error::InvalidChallengeSolution);
|
Some(_) => {
|
||||||
};
|
error!("User-agent pubkey integrity tag mismatch");
|
||||||
|
Ok(AttestationStatus::NotAttested)
|
||||||
if stored_tag != expected_tag {
|
}
|
||||||
error!("User-agent pubkey integrity tag mismatch");
|
None => {
|
||||||
return Err(Error::InvalidChallengeSolution);
|
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
|
||||||
|
Ok(AttestationStatus::NotAttested)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
evm::{
|
evm::{
|
||||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||||
},
|
},
|
||||||
keyholder::{self, Bootstrap, SignUseragentPubkeyIntegrityTag, TryUnseal},
|
keyholder::{self, Bootstrap, SignIntegrityTag, TryUnseal},
|
||||||
user_agent::session::{
|
user_agent::session::{
|
||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
@@ -111,14 +111,14 @@ impl UserAgentSession {
|
|||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
.key_holder
|
.key_holder
|
||||||
.ask(SignUseragentPubkeyIntegrityTag {
|
.ask(SignIntegrityTag {
|
||||||
public_key,
|
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||||
key_type,
|
data_parts: vec![(key_type as i32).to_be_bytes().to_vec(), public_key],
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(?err, "Failed to sign user-agent pubkey integrity tag");
|
error!(?err, "Failed to sign integrity tag");
|
||||||
Error::internal("Failed to sign user-agent pubkey integrity tag")
|
Error::internal("Failed to sign integrity tag")
|
||||||
})?;
|
})?;
|
||||||
updates.push((id, tag));
|
updates.push((id, tag));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user