From e60393ecb7ad5bbefc63d5c3f23ffc9c684d6b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20d=27Auria?= Date: Thu, 3 Jul 2025 11:05:14 +0200 Subject: [PATCH 1/2] feat: add third-party block support Add methods to create and inspect third party blocks: - `Biscuit::append_third_party` for creating biscuits with third party blocks - `Biscuit::external_key` for retrieving external keys from blocks - Add unit tests covering third party block functionality --- biscuit_test.py | 25 +++++++++++++++++++++++++ src/lib.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/biscuit_test.py b/biscuit_test.py index 8d49186..dd25520 100644 --- a/biscuit_test.py +++ b/biscuit_test.py @@ -463,3 +463,28 @@ def test(left, right): policy = authorizer.build_unauthenticated().authorize() assert policy == 0 +def test_append_third_party(): + root_kp = KeyPair() + biscuit_builder = BiscuitBuilder("user({id})", { 'id': "1234" }) + biscuit = biscuit_builder.build(root_kp.private_key) + + third_party_kp = KeyPair() + third_party_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) + biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp, third_party_block) + + assert repr(biscuit_with_third_party_block.block_external_key(1)) == repr(third_party_kp.public_key) + +def test_read_third_party_facts(): + root_kp = KeyPair() + biscuit_builder = BiscuitBuilder("user({id})", { 'id': "1234" }) + biscuit = biscuit_builder.build(root_kp.private_key) + + third_party_kp = KeyPair() + third_party_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) + biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp, third_party_block) + + authorizer = AuthorizerBuilder("allow if true").build(biscuit_with_third_party_block) + rule = Rule(f"ext_fact($fact) <- external_fact($fact) trusting {third_party_kp.public_key}") + facts = authorizer.query(rule) + + assert facts[0].terms[0] == "56" diff --git a/src/lib.rs b/src/lib.rs index cf049dd..d0f661c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -397,6 +397,36 @@ impl PyBiscuit { .map(PyBiscuit) } + /// Create a new `Biscuit` by appending a third party attenuation block + /// + /// :param kp: keypair used to sign the block + /// :type kp: KeyPair + /// :param block: a builder for the new block + /// :type block: BlockBuilder + /// :return: the attenuated biscuit + /// :rtype: Biscuit + pub fn append_third_party( + &self, + kp: &PyKeyPair, + block: &PyBlockBuilder, + ) -> PyResult { + let third_party_block = self + .0 + .third_party_request() + .and_then(|req| { + req.create_block( + &kp.private_key().0, + block.0.clone().expect("builder already consumed"), + ) + }) + .map_err(|e| BiscuitBuildError::new_err(e.to_string()))?; + + self.0 + .append_third_party(kp.public_key().0, third_party_block) + .map_err(|e| BiscuitBuildError::new_err(e.to_string())) + .map(PyBiscuit) + } + /// The revocation ids of the token, encoded as hexadecimal strings #[getter] pub fn revocation_ids(&self) -> Vec { @@ -407,6 +437,21 @@ impl PyBiscuit { .collect() } + /// Get the external key of a block if it exists + /// + /// :param index: the block index + /// :type index: int + /// :return: the public key if it exists + /// :rtype: str | None + pub fn block_external_key(&self, index: usize) -> PyResult> { + let opt_key = self + .0 + .block_external_key(index) + .map_err(|e| BiscuitBlockError::new_err(e.to_string()))?; + + Ok(opt_key.map(PyPublicKey)) + } + fn __repr__(&self) -> String { self.0.print() } From 9cf7f25610518601d13b9f4a2f2dc1ece5a5da42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20d=27Auria?= Date: Thu, 3 Jul 2025 13:23:46 +0200 Subject: [PATCH 2/2] feat: add support for third-party requests --- biscuit_test.py | 19 ++++++++----- src/lib.rs | 75 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/biscuit_test.py b/biscuit_test.py index dd25520..ac308fa 100644 --- a/biscuit_test.py +++ b/biscuit_test.py @@ -217,7 +217,7 @@ def choose_key(kid): elif kid == 1: return root.public_key else: - raise Exception("Unknown key identifier") + raise Exception("Unknown key identifier") biscuit_builder0 = BiscuitBuilder("user({id})", { 'id': "1234" }) token0 = biscuit_builder0.build(other_root.private_key).to_base64() @@ -386,10 +386,10 @@ def test_biscuit_inspection(): builder.set_root_key_id(42) token2 = builder.build(kp.private_key).append(BlockBuilder('test(false);')) print(token2.to_base64()) - + utoken1 = UnverifiedBiscuit.from_base64(token1.to_base64()) utoken2 = UnverifiedBiscuit.from_base64(token2.to_base64()) - + assert utoken1.root_key_id() is None assert utoken2.root_key_id() == 42 @@ -469,8 +469,10 @@ def test_append_third_party(): biscuit = biscuit_builder.build(root_kp.private_key) third_party_kp = KeyPair() - third_party_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) - biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp, third_party_block) + new_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) + third_party_request = biscuit.third_party_request() + third_party_block = third_party_request.create_block(third_party_kp.private_key, new_block) + biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp.public_key, third_party_block) assert repr(biscuit_with_third_party_block.block_external_key(1)) == repr(third_party_kp.public_key) @@ -480,8 +482,11 @@ def test_read_third_party_facts(): biscuit = biscuit_builder.build(root_kp.private_key) third_party_kp = KeyPair() - third_party_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) - biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp, third_party_block) + new_block = BlockBuilder("external_fact({fact})", { 'fact': "56" }) + third_party_request = biscuit.third_party_request() + third_party_block = third_party_request.create_block(third_party_kp.private_key, new_block) + biscuit_with_third_party_block = biscuit.append_third_party(third_party_kp.public_key, third_party_block) + authorizer = AuthorizerBuilder("allow if true").build(biscuit_with_third_party_block) rule = Rule(f"ext_fact($fact) <- external_fact($fact) trusting {third_party_kp.public_key}") diff --git a/src/lib.rs b/src/lib.rs index d0f661c..2bdb8a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ use ::biscuit_auth::builder::MapKey; use ::biscuit_auth::datalog::ExternFunc; use ::biscuit_auth::AuthorizerBuilder; use ::biscuit_auth::RootKeyProvider; +use ::biscuit_auth::ThirdPartyBlock; +use ::biscuit_auth::ThirdPartyRequest; use ::biscuit_auth::UnverifiedBiscuit; use chrono::DateTime; use chrono::Duration; @@ -397,36 +399,36 @@ impl PyBiscuit { .map(PyBiscuit) } - /// Create a new `Biscuit` by appending a third party attenuation block + /// Create a new `Biscuit` by appending a third-party attenuation block /// - /// :param kp: keypair used to sign the block - /// :type kp: KeyPair - /// :param block: a builder for the new block - /// :type block: BlockBuilder + /// :param external_key: the public key of the third-party that signed the block. + /// :type external_key: PublicKey + /// :param block: the third party block to append + /// :type block: ThirdPartyBlock /// :return: the attenuated biscuit /// :rtype: Biscuit pub fn append_third_party( &self, - kp: &PyKeyPair, - block: &PyBlockBuilder, + external_key: &PyPublicKey, + block: &PyThirdPartyBlock, ) -> PyResult { - let third_party_block = self - .0 - .third_party_request() - .and_then(|req| { - req.create_block( - &kp.private_key().0, - block.0.clone().expect("builder already consumed"), - ) - }) - .map_err(|e| BiscuitBuildError::new_err(e.to_string()))?; - self.0 - .append_third_party(kp.public_key().0, third_party_block) + .append_third_party(external_key.0, block.0.clone()) .map_err(|e| BiscuitBuildError::new_err(e.to_string())) .map(PyBiscuit) } + /// Create a third-party request for generating third-party blocks. + /// + /// :return: the third-party request + /// :rtype: ThirdPartyRequest + pub fn third_party_request(&self) -> PyResult { + self.0 + .third_party_request() + .map_err(|e| BiscuitBuildError::new_err(e.to_string())) + .map(|request| PyThirdPartyRequest(Some(request))) + } + /// The revocation ids of the token, encoded as hexadecimal strings #[getter] pub fn revocation_ids(&self) -> Vec { @@ -1063,6 +1065,41 @@ impl PyBlockBuilder { } } +#[pyclass(name = "ThirdPartyRequest")] +pub struct PyThirdPartyRequest(Option); + +#[pymethods] +impl PyThirdPartyRequest { + /// Create a third-party block + /// + /// :param private_key: the third-party's private key used to sign the block + /// :type external_key: PrivateKey + /// :param block: the block builder to be signed + /// :type block: BlockBuilder + /// :return: a signed block that can be appended to a Biscuit + /// :rtype: ThirdPartyBlock + /// + /// :note: this method consumes the `ThirdPartyRequest` object. + pub fn create_block( + &mut self, + private_key: &PyPrivateKey, + block: &PyBlockBuilder, + ) -> PyResult { + self.0 + .take() + .expect("third party request already consumed") + .create_block( + &private_key.0, + block.0.clone().expect("builder already consumed"), + ) + .map_err(|e| BiscuitBuildError::new_err(e.to_string())) + .map(PyThirdPartyBlock) + } +} + +#[pyclass(name = "ThirdPartyBlock")] +pub struct PyThirdPartyBlock(ThirdPartyBlock); + /// ed25519 keypair #[pyclass(name = "KeyPair")] pub struct PyKeyPair(KeyPair);