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
2 changes: 1 addition & 1 deletion biscuit-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ pub use token::builder_ext;
pub use token::unverified::UnverifiedBiscuit;
pub use token::Biscuit;
pub use token::RootKeyProvider;
pub use token::{ThirdPartyBlock, ThirdPartyRequest};
pub use token::{ThirdPartyBlock, ThirdPartyRequest, ThirdPartyUnsignedBlock};

#[cfg(feature = "bwk")]
mod bwk;
Expand Down
195 changes: 186 additions & 9 deletions biscuit-auth/src/token/third_party.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ impl ThirdPartyRequest {
private_key: &PrivateKey,
block_builder: BlockBuilder,
) -> Result<ThirdPartyBlock, error::Token> {
let unsigned = self.prepare_block(block_builder)?;

let keypair = KeyPair::from(private_key);
let signature = keypair.sign(unsigned.bytes_to_sign())?;
let public_key = keypair.public();

unsigned.with_external_signature(public_key, signature.to_bytes())
}

/// Prepares a third-party block for an external signer.
///
/// Some keys can never be handed over as a [`PrivateKey`]: HSMs, TPMs and
/// secure enclaves will sign bytes for you, but won't give up the key.
/// This splits [`ThirdPartyRequest::create_block`] in two so those signers
/// work: take [`ThirdPartyUnsignedBlock::bytes_to_sign`], sign it wherever
/// the key lives, then assemble the block with
/// [`ThirdPartyUnsignedBlock::with_external_signature`].
///
/// If the key is available in memory, `create_block` still does both steps
/// in one call (it is built on top of this).
pub fn prepare_block(
self,
block_builder: BlockBuilder,
) -> Result<ThirdPartyUnsignedBlock, error::Token> {
let symbols = SymbolTable::new();
let mut block = block_builder.build(symbols);
block.version = max(super::DATALOG_3_2, block.version);
Expand All @@ -109,25 +133,60 @@ impl ThirdPartyRequest {
error::Format::SerializationError(format!("serialization error: {e:?}"))
})?;

let signed_payload = generate_external_signature_payload_v1(
let bytes_to_sign = generate_external_signature_payload_v1(
&payload,
&self.previous_signature,
THIRD_PARTY_SIGNATURE_VERSION,
);

let keypair = KeyPair::from(private_key);
let signature = keypair.sign(&signed_payload)?;

let public_key = keypair.public();
let content = schema::ThirdPartyBlockContents {
Ok(ThirdPartyUnsignedBlock {
payload,
bytes_to_sign,
})
}
}

/// A third-party block waiting for its external signature.
///
/// Sign exactly [`ThirdPartyUnsignedBlock::bytes_to_sign`] (Ed25519 signs the
/// bytes directly; ECDSA P-256 signs their SHA-256, which is what hardware
/// modules do natively), then call
/// [`ThirdPartyUnsignedBlock::with_external_signature`].
#[derive(Clone, Debug)]
pub struct ThirdPartyUnsignedBlock {
payload: Vec<u8>,
bytes_to_sign: Vec<u8>,
}

impl ThirdPartyUnsignedBlock {
/// The exact bytes the external signer must sign.
pub fn bytes_to_sign(&self) -> &[u8] {
&self.bytes_to_sign
}

/// Assembles the block from an externally produced signature.
///
/// The signature is checked against `public_key` right here, so a wrong
/// key or a corrupted signature fails immediately instead of later, when
/// the token is deserialized.
pub fn with_external_signature(
self,
public_key: crate::PublicKey,
signature_bytes: &[u8],
) -> Result<ThirdPartyBlock, error::Token> {
let signature =
crate::crypto::Signature::from_bytes(signature_bytes).map_err(error::Token::Format)?;
public_key
.verify_signature(&self.bytes_to_sign, &signature)
.map_err(error::Token::Format)?;

Ok(ThirdPartyBlock(schema::ThirdPartyBlockContents {
payload: self.payload,
external_signature: schema::ExternalSignature {
signature: signature.to_bytes().to_vec(),
public_key: public_key.to_proto(),
},
};

Ok(ThirdPartyBlock(content))
}))
}
}

Expand Down Expand Up @@ -157,6 +216,124 @@ impl ThirdPartyBlock {
mod tests {
use super::*;

/// A token and an external keypair for the external-signing tests.
fn setup(
external_alg: crate::builder::Algorithm,
) -> (KeyPair, crate::Biscuit, KeyPair, BlockBuilder) {
let mut rng: rand::rngs::StdRng = rand::SeedableRng::seed_from_u64(42);
let root = KeyPair::new_with_rng(crate::builder::Algorithm::Ed25519, &mut rng);
let biscuit = crate::Biscuit::builder()
.fact("right(\"file1\", \"read\")")
.unwrap()
.build_with_rng(&root, crate::token::default_symbol_table(), &mut rng)
.unwrap();
let external = KeyPair::new_with_rng(external_alg, &mut rng);
let block = BlockBuilder::new()
.fact("external_fact(\"hello\")")
.unwrap();
(root, biscuit, external, block)
}

/// The prepare/sign/assemble path must produce a block a standard
/// verifier accepts, without the private key ever entering the library.
/// The "external signer" here only ever sees the bytes to sign -- exactly
/// what an HSM would.
#[test]
fn external_signature_round_trip_ed25519() {
let (root, biscuit, external, block) = setup(crate::builder::Algorithm::Ed25519);

let unsigned = biscuit
.third_party_request()
.unwrap()
.prepare_block(block)
.unwrap();
let signature = external.sign(unsigned.bytes_to_sign()).unwrap();
let third_party_block = unsigned
.with_external_signature(external.public(), signature.to_bytes())
.unwrap();

let biscuit2 = biscuit
.append_third_party(external.public(), third_party_block)
.unwrap();
// Re-parsing verifies every signature in the chain, external included.
crate::Biscuit::from(biscuit2.to_vec().unwrap(), root.public()).unwrap();
}

#[test]
fn external_signature_round_trip_p256() {
let (root, biscuit, external, block) = setup(crate::builder::Algorithm::Secp256r1);

let unsigned = biscuit
.third_party_request()
.unwrap()
.prepare_block(block)
.unwrap();
let signature = external.sign(unsigned.bytes_to_sign()).unwrap();
let third_party_block = unsigned
.with_external_signature(external.public(), signature.to_bytes())
.unwrap();

let biscuit2 = biscuit
.append_third_party(external.public(), third_party_block)
.unwrap();
crate::Biscuit::from(biscuit2.to_vec().unwrap(), root.public()).unwrap();
}

/// Deterministic Ed25519: the split path and create_block must produce
/// byte-identical blocks for the same key and content.
#[test]
fn external_signature_matches_create_block_ed25519() {
let (_root, biscuit, external, block) = setup(crate::builder::Algorithm::Ed25519);

let via_create = biscuit
.third_party_request()
.unwrap()
.create_block(&external.private(), block.clone())
.unwrap();

let unsigned = biscuit
.third_party_request()
.unwrap()
.prepare_block(block)
.unwrap();
let signature = external.sign(unsigned.bytes_to_sign()).unwrap();
let via_external = unsigned
.with_external_signature(external.public(), signature.to_bytes())
.unwrap();

assert_eq!(
via_create.serialize_base64().unwrap(),
via_external.serialize_base64().unwrap()
);
}

/// A corrupted signature or a mismatched key fails at assembly, not
/// later at token deserialization.
#[test]
fn external_signature_rejects_tampering_and_wrong_key() {
let (_root, biscuit, external, block) = setup(crate::builder::Algorithm::Ed25519);

let unsigned = biscuit
.third_party_request()
.unwrap()
.prepare_block(block)
.unwrap();
let signature = external.sign(unsigned.bytes_to_sign()).unwrap();

let mut tampered = signature.to_bytes().to_vec();
tampered[0] ^= 0x01;
assert!(unsigned
.clone()
.with_external_signature(external.public(), &tampered)
.is_err());

let mut rng: rand::rngs::StdRng = rand::SeedableRng::seed_from_u64(7);
let other = KeyPair::new_with_rng(crate::builder::Algorithm::Ed25519, &mut rng);
assert!(unsigned
.with_external_signature(other.public(), signature.to_bytes())
.is_err());
}

#[test]
fn third_party_request_roundtrip() {
let mut rng: rand::rngs::StdRng = rand::SeedableRng::seed_from_u64(0);
Expand Down